Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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>;
}
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 را می‌خواهیم استفاده کنیم.

با استفاده از نوع‌های وابسته (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 انجام می‌دهیم.

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 معروف است، که در بخش [«استفاده از الگوی Newtype برای پیاده‌سازی Traitهای خارجی»][newtype] به‌صورت دقیق‌تر توضیح داده‌ایم. ما می‌خواهیم مقادیر millimeters را با مقادیر meters جمع کنیم و پیاده‌سازی 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 بدون شکستن کد پیاده‌سازی موجود، یک مقدار پیش‌فرض برای آن تنظیم کنید.

Disambiguating Between 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 را باید استفاده کند.

با این حال، توابع وابسته‌ای (associated functions) که متد نیستند، پارامتر self ندارند. زمانی که چندین نوع یا trait توابع غیرمتدی با نام یکسان تعریف می‌کنند، Rust همیشه نمی‌تواند تشخیص دهد که منظور شما کدام نوع است، مگر آن‌که از نحوی به‌نام fully qualified syntax استفاده کنید. برای مثال، در لیستینگ 20-20 یک trait برای یک پناهگاه حیوانات ایجاد می‌کنیم که می‌خواهد نام تمام توله‌سگ‌ها را Spot بگذارد. یک trait به‌نام Animal تعریف می‌کنیم که شامل یک تابع وابسته غیرمتدی baby_name است. این trait برای structی به‌نام Dog پیاده‌سازی می‌شود، و بر روی خود Dog نیز مستقیماً یک تابع وابسته غیرمتدی به‌نام baby_name ارائه می‌دهیم.

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

استفاده از 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 را نشان می‌دهد.

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 برای پیاده‌سازی 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 نشان داده شده است.

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> در موقعیت اندیس ۰ این 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 بپردازیم.