Advanced Traits
ما در بخش “Traits: Defining Shared Behavior” از فصل 10 به بررسی traits پرداختیم، اما جزئیات پیشرفتهتر آن را مورد بحث قرار ندادیم. اکنون که اطلاعات بیشتری در مورد راست دارید، میتوانیم به عمق موضوع بپردازیم.
Specifying Placeholder Types in Trait Definitions with 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
را میخواهیم استفاده کنیم.
با استفاده از نوعهای مرتبط، نیازی به حاشیهنویسی نوعها نداریم زیرا نمیتوانیم یک trait را بر روی یک نوع چندین بار پیادهسازی کنیم. در فهرست 20-13 با تعریفی که از نوعهای مرتبط استفاده میکند، ما فقط میتوانیم نوع Item
را یک بار انتخاب کنیم، زیرا تنها یک impl Iterator for Counter
میتواند وجود داشته باشد. ما نیازی نداریم که مشخص کنیم میخواهیم یک iterator از مقادیر u32
داشته باشیم در هر جایی که next
را بر روی Counter
فراخوانی میکنیم.
نوعهای مرتبط همچنین بخشی از قرارداد trait میشوند: پیادهسازان trait باید یک نوع ارائه دهند تا جایگزین نوع جایگزین مرتبط شود. نوعهای مرتبط اغلب نامی دارند که توصیف میکند چگونه نوع استفاده خواهد شد و مستندسازی نوع مرتبط در مستندات API یک عمل خوب است.
Default Generic Type Parameters and Operator Overloading
وقتی که از پارامترهای generic type استفاده میکنیم، میتوانیم یک نوع خاص پیشفرض برای پارامتر generic تعیین کنیم. این نیاز به مشخص کردن یک نوع خاص توسط پیادهسازان trait را در صورتی که نوع پیشفرض کار کند، از بین میبرد. شما میتوانید هنگام اعلام یک نوع generic، یک نوع پیشفرض با سینتکس <PlaceholderType=ConcreteType>
مشخص کنید.
یک مثال عالی از وضعیتی که این تکنیک مفید است، بارگذاری مجدد عملگرها است، جایی که شما رفتار یک عملگر (مانند +
) را در شرایط خاص شخصیسازی میکنید.
راست به شما اجازه نمیدهد عملگرهای خود را ایجاد کنید یا عملگرهای دلخواه را بارگذاری مجدد کنید. اما میتوانید عملیاتها و traits مربوط به آنها را که در std::ops
فهرست شدهاند با پیادهسازی traits مرتبط با عملگر، بارگذاری مجدد کنید. برای مثال، در فهرست 20-15 ما عملگر +
را برای اضافه کردن دو نمونه Point
به یکدیگر بارگذاری مجدد میکنیم. ما این کار را با پیادهسازی trait Add
بر روی struct 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 pattern شناخته میشود که ما آن را در بخش “Using the Newtype Pattern to Implement External Traits on External Types” بهطور مفصل توضیح میدهیم. ما میخواهیم مقادیر میلیمتر را به مقادیر متر اضافه کنیم و پیادهسازی 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 بدون شکستن کد پیادهسازی موجود، یک مقدار پیشفرض برای آن تنظیم کنید.
Fully Qualified Syntax for Disambiguation: Calling 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 را باید استفاده کند.
با این حال، توابع مرتبطی که متد نیستند، پارامتر self
ندارند. وقتی چندین نوع یا trait توابع غیر متد با یک نام مشترک تعریف میکنند، راست همیشه نمیداند که منظور شما کدام نوع است، مگر اینکه از fully qualified syntax استفاده کنید. به عنوان مثال، در فهرست 20-20 ما یک trait برای یک پناهگاه حیوانات ایجاد میکنیم که میخواهد تمام سگهای کوچک را به نام Spot نامگذاری کند. ما یک trait به نام Animal
با یک تابع غیر متد مرتبط به نام baby_name
تعریف میکنیم. trait Animal
برای ساختار Dog
پیادهسازی میشود، و همچنین یک تابع غیر متد مرتبط به نام baby_name
مستقیماً بر روی Dog
فراهم میکنیم.
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 را در هر جایی که توابع یا متدها را فراخوانی میکنید، استفاده کنید. با این حال، مجاز هستید هر بخشی از این سینتکس را که راست میتواند از اطلاعات دیگر برنامه تشخیص دهد، حذف کنید. شما فقط در مواردی که چندین پیادهسازی با نام یکسان وجود دارد و راست به کمک نیاز دارد تا مشخص کند کدام پیادهسازی را میخواهید فراخوانی کنید، نیاز به استفاده از این سینتکس دقیقتر دارید.
استفاده از Supertraits برای نیاز به قابلیتهای یک trait درون trait دیگر
گاهی اوقات ممکن است یک تعریف trait بنویسید که به یک trait دیگر وابسته باشد: برای اینکه یک نوع بتواند اولین trait را پیادهسازی کند، میخواهید که آن نوع همچنین دومین trait را نیز پیادهسازی کند. این کار را انجام میدهید تا تعریف trait شما بتواند از آیتمهای مرتبط trait دوم استفاده کند. traitای که تعریف trait شما به آن متکی است، supertrait نامیده میشود.
برای مثال، فرض کنید میخواهید یک trait به نام OutlinePrint
بسازید که یک متد outline_print
داشته باشد که یک مقدار داده شده را با فرمت مشخصی که در قاب ستارهها قرار گرفته است، چاپ کند. به این صورت که اگر یک ساختار Point
داشته باشیم که trait کتابخانه استاندارد Display
را پیادهسازی کرده و نتیجه آن به شکل (x, y)
باشد، وقتی متد outline_print
را روی یک نمونه از Point
که 1
برای x
و 3
برای y
دارد فراخوانی کنیم، باید خروجی زیر را چاپ کند:
**********
* *
* (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 برای پیادهسازی Traits خارجی روی انواع خارجی
در فصل ۱۰ در بخش “پیادهسازی یک Trait روی یک نوع”، به قانون orphan اشاره کردیم که بیان میکند ما فقط مجاز هستیم یک trait را روی یک نوع پیادهسازی کنیم اگر یا trait یا نوع به crate ما تعلق داشته باشد. با این حال، میتوان با استفاده از الگوی newtype، این محدودیت را دور زد. این الگو شامل ایجاد یک نوع جدید در یک tuple struct است. (ما tuple structها را در بخش “استفاده از Tuple Structs بدون فیلدهای نامگذاریشده برای ایجاد انواع مختلف” در فصل ۵ پوشش دادیم.) tuple struct یک فیلد خواهد داشت و یک wrapper نازک دور نوعی خواهد بود که میخواهیم trait را برای آن پیادهسازی کنیم. سپس، نوع wrapper به crate ما تعلق دارد و میتوانیم trait را روی wrapper پیادهسازی کنیم. اصطلاح Newtype از زبان برنامهنویسی Haskell منشأ گرفته است. هیچ جریمه عملکردی در زمان اجرا برای استفاده از این الگو وجود ندارد و نوع wrapper در زمان کامپایل حذف میشود.
بهعنوان مثال، فرض کنید میخواهیم 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>
آیتمی در index صفر tuple است. سپس میتوانیم از قابلیتهای trait Display
روی Wrapper
استفاده کنیم.
عیب استفاده از این تکنیک این است که Wrapper
یک نوع جدید است، بنابراین متدهای نوعی که درون خود نگه میدارد را ندارد. باید تمام متدهای Vec<T>
را مستقیماً روی Wrapper
پیادهسازی کنیم به طوری که متدها به self.0
ارجاع دهند، که به ما اجازه میدهد Wrapper
را دقیقاً مانند Vec<T>
رفتار دهیم. اگر بخواهیم نوع جدید تمام متدهایی که نوع داخلی دارد را داشته باشد، پیادهسازی trait Deref
(که در فصل ۱۵ در بخش “رفتار با اشارهگر (Pointer)های هوشمند بهعنوان ارجاعات معمولی با استفاده از trait Deref
” بحث شد) روی Wrapper
بهگونهای که نوع داخلی را بازگرداند، راهحلی خواهد بود. اگر نخواهیم نوع Wrapper
تمام متدهای نوع داخلی را داشته باشد—برای مثال، برای محدود کردن رفتار نوع Wrapper
—باید متدهایی که واقعاً نیاز داریم را به صورت دستی پیادهسازی کنیم.
این الگوی newtype حتی زمانی که traits درگیر نیستند نیز مفید است. حالا بیایید تمرکز خود را تغییر دهیم و به برخی از روشهای پیشرفته برای تعامل با سیستم نوع Rust بپردازیم.