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>;
}
Listing 20-13: تعریف trait Iterator که دارای یک نوع مرتبط به نام Item است

نوع Item یک جایگزین است و تعریف متد next نشان می‌دهد که مقادیری از نوع Option<Self::Item> را بازمی‌گرداند. پیاده‌سازان trait Iterator نوع خاصی را برای Item مشخص می‌کنند و متد next یک Option حاوی مقدار از آن نوع خاص بازمی‌گرداند.

نوع‌های مرتبط ممکن است مفهومی مشابه با genericها به نظر برسند، به این معنا که genericها به ما اجازه می‌دهند یک تابع بدون مشخص کردن نوع‌هایی که می‌تواند با آن‌ها کار کند، تعریف کنیم. برای بررسی تفاوت بین این دو مفهوم، به یک پیاده‌سازی trait Iterator روی یک نوع به نام Counter نگاه خواهیم کرد که نوع Item را به‌عنوان u32 مشخص می‌کند:

Filename: src/lib.rs
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>;
}
Listing 20-14: یک تعریف فرضی از trait 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 انجام می‌دهیم:

Filename: src/main.rs
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 }
    );
}
Listing 20-15: پیاده‌سازی trait 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 نشان داده شده است، پیاده‌سازی کنیم.

Filename: src/lib.rs
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))
    }
}
Listing 20-16: پیاده‌سازی trait 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 کاری متفاوت انجام می‌دهد.

Filename: src/main.rs
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() {}
Listing 20-17: دو trait تعریف شده‌اند که یک متد مشترک دارند و بر روی نوع Human پیاده‌سازی شده‌اند، و یک متد fly به‌طور مستقیم بر روی Human پیاده‌سازی شده است

وقتی متد fly را بر روی یک نمونه از Human فراخوانی می‌کنیم، کامپایلر به طور پیش‌فرض متدی را که مستقیماً بر روی نوع پیاده‌سازی شده است، فراخوانی می‌کند، همان‌طور که در فهرست 20-18 نشان داده شده است.

Filename: src/main.rs
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();
}
Listing 20-18: فراخوانی fly بر روی یک نمونه از Human

اجرای این کد متن *waving arms furiously* را چاپ می‌کند و نشان می‌دهد که راست متد fly پیاده‌سازی‌شده بر روی Human را مستقیماً فراخوانی کرده است.

برای فراخوانی متدهای fly از Pilot یا Wizard، باید از سینتکس صریح‌تری برای مشخص کردن متدی که منظور ماست، استفاده کنیم. فهرست 20-19 این سینتکس را نشان می‌دهد.

Filename: src/main.rs
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();
}
Listing 20-19: مشخص کردن متد 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 فراهم می‌کنیم.

Filename: src/main.rs
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());
}
Listing 20-20: یک trait با یک تابع مرتبط و یک نوع با یک تابع مرتبط با همان نام که همچنین trait را پیاده‌سازی می‌کند

ما کدی برای نام‌گذاری تمام سگ‌های کوچک به نام 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 تغییر دهیم، خطای کامپایل دریافت خواهیم کرد.

Filename: src/main.rs
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());
}
Listing 20-21: تلاش برای فراخوانی تابع 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 استفاده کنیم.

Filename: src/main.rs
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());
}
Listing 20-22: استفاده از fully qualified syntax برای مشخص کردن اینکه می‌خواهیم تابع 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 را نشان می‌دهد.

Filename: src/main.rs
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() {}
Listing 20-23: پیاده‌سازی trait OutlinePrint که نیاز به قابلیت‌های Display دارد

از آنجایی که مشخص کرده‌ایم که OutlinePrint به trait Display نیاز دارد، می‌توانیم از تابع to_string استفاده کنیم که به طور خودکار برای هر نوعی که Display را پیاده‌سازی کرده باشد، پیاده‌سازی شده است. اگر سعی کنیم to_string را بدون اضافه کردن دو نقطه و مشخص کردن trait Display بعد از نام trait استفاده کنیم، خطایی دریافت خواهیم کرد که می‌گوید هیچ متدی به نام to_string برای نوع &Self در محدوده فعلی یافت نشد.

حالا ببینیم چه اتفاقی می‌افتد اگر بخواهیم OutlinePrint را برای یک نوعی که Display را پیاده‌سازی نکرده است، مانند ساختار Point، پیاده‌سازی کنیم:

Filename: src/main.rs
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 را برآورده کنیم، به این صورت:

Filename: src/main.rs
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 نشان داده شده است.

Filename: src/main.rs
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}");
}
Listing 20-24: ایجاد نوع 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 بپردازیم.