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

رفتار با اشاره‌گرهای هوشمند مانند رفرنس‌های معمولی با استفاده از Deref

پیاده‌سازی ویژگی Deref به شما امکان می‌دهد رفتار عملگر اشاره‌گر (Pointer)‌زدایی * را سفارشی کنید (این را با عملگر ضرب یا glob اشتباه نگیرید). با پیاده‌سازی Deref به گونه‌ای که یک اشاره‌گر (Pointer) هوشمند بتواند مانند یک ارجاع معمولی رفتار کند، می‌توانید کدی بنویسید که روی ارجاعات عمل می‌کند و از آن کد با اشاره‌گر (Pointer)های هوشمند نیز استفاده کنید.

ابتدا بیایید نگاهی به این بیندازیم که چگونه عملگر اشاره‌گر (Pointer)‌زدایی با ارجاعات معمولی کار می‌کند. سپس سعی می‌کنیم یک نوع سفارشی تعریف کنیم که مانند Box<T> رفتار کند، و بررسی کنیم چرا عملگر اشاره‌گر (Pointer)‌زدایی مانند یک ارجاع روی نوع جدید ما عمل نمی‌کند. ما بررسی می‌کنیم که چگونه پیاده‌سازی ویژگی Deref امکان‌پذیر می‌سازد که اشاره‌گر (Pointer)های هوشمند به شیوه‌ای مشابه ارجاعات عمل کنند. سپس نگاهی به ویژگی فشار اشاره‌گر (Pointer)‌زدایی (deref coercion) در Rust می‌اندازیم و اینکه چگونه به ما اجازه می‌دهد با ارجاعات یا اشاره‌گر (Pointer)های هوشمند کار کنیم.

توجه: یک تفاوت بزرگ بین نوع MyBox<T> که قرار است بسازیم و Box<T> واقعی وجود دارد: نسخه ما داده‌های خود را در heap ذخیره نمی‌کند. ما این مثال را بر روی Deref متمرکز کرده‌ایم، بنابراین مکانی که داده‌ها واقعاً در آن ذخیره می‌شوند کمتر از رفتار اشاره‌گر (Pointer)گونه اهمیت دارد.

دنبال کردن اشاره‌گر (Pointer) به مقدار

یک ارجاع معمولی نوعی اشاره‌گر (Pointer) است، و یکی از راه‌های فکر کردن به یک اشاره‌گر (Pointer) این است که به عنوان یک فلش به یک مقدار ذخیره‌شده در جای دیگری در نظر گرفته شود. در لیستینگ ۱۵-۶، ما یک ارجاع به یک مقدار i32 ایجاد می‌کنیم و سپس از عملگر اشاره‌گر (Pointer)‌زدایی برای دنبال کردن ارجاع به مقدار استفاده می‌کنیم:

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-6: استفاده از عملگر اشاره‌گر (Pointer)‌زدایی برای دنبال کردن یک ارجاع به یک مقدار i32

متغیر x مقدار i32 برابر با 5 را نگه می‌دارد. ما y را برابر با یک ارجاع به x تنظیم می‌کنیم. می‌توانیم تایید کنیم که x برابر با 5 است. با این حال، اگر بخواهیم یک تایید روی مقدار داخل y انجام دهیم، باید از *y برای دنبال کردن ارجاع به مقداری که به آن اشاره می‌کند استفاده کنیم (بنابراین اشاره‌گر (Pointer)‌زدایی) تا کامپایلر بتواند مقدار واقعی را مقایسه کند. وقتی y را اشاره‌گر (Pointer)‌زدایی می‌کنیم، به مقدار صحیحی که y به آن اشاره می‌کند دسترسی داریم و می‌توانیم آن را با 5 مقایسه کنیم.

اگر بخواهیم assert_eq!(5, y); بنویسیم، خطای کامپایل زیر را دریافت می‌کنیم:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

مقایسه یک عدد با یک ارجاع به عدد مجاز نیست زیرا آن‌ها انواع متفاوتی هستند. ما باید از عملگر اشاره‌گر (Pointer)‌زدایی برای دنبال کردن ارجاع به مقداری که به آن اشاره می‌کند استفاده کنیم.

استفاده از Box<T> مانند یک ارجاع

ما می‌توانیم کد لیستینگ ۱۵-۶ را برای استفاده از یک Box<T> به‌جای یک ارجاع بازنویسی کنیم؛ عملگر اشاره‌گر (Pointer)‌زدایی که روی Box<T> در لیستینگ ۱۵-۷ استفاده شده است، به همان شیوه‌ای عمل می‌کند که روی ارجاع در لیستینگ ۱۵-۶ عمل می‌کرد:

Filename: src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-7: استفاده از عملگر اشاره‌گر (Pointer)‌زدایی روی یک Box<i32>

تفاوت اصلی بین لیستینگ ۱۵-۷ و لیستینگ ۱۵-۶ این است که در اینجا y را به‌عنوان یک نمونه از Box<T> تنظیم می‌کنیم که به یک مقدار کپی‌شده از x اشاره می‌کند، به‌جای یک ارجاع که به مقدار x اشاره می‌کند. در تایید نهایی، می‌توانیم از عملگر اشاره‌گر (Pointer)‌زدایی برای دنبال کردن اشاره‌گر (Pointer) Box<T> به همان شیوه‌ای که زمانی که y یک ارجاع بود استفاده کردیم. در ادامه بررسی می‌کنیم چه چیزی در مورد Box<T> خاص است که به ما اجازه می‌دهد از عملگر اشاره‌گر (Pointer)‌زدایی استفاده کنیم، با تعریف نوع خودمان.

تعریف اشاره‌گر (Pointer) هوشمند خودمان

بیایید یک نوع پوشاننده (wrapper type) مشابه با نوع Box<T> که توسط کتابخانه استاندارد ارائه شده است بسازیم تا تجربه کنیم که چگونه انواع اشاره‌گر هوشمند به‌طور پیش‌فرض رفتاری متفاوت از رفرنس‌ها دارند. سپس بررسی خواهیم کرد که چگونه می‌توان قابلیت استفاده از عملگر dereference را به آن افزود.

نکته: یک تفاوت بزرگ بین نوع MyBox<T> که در شرف ساخت آن هستیم و Box<T> واقعی وجود دارد: نسخه‌ی ما داده‌ها را در heap ذخیره نخواهد کرد. ما در این مثال بر Deref تمرکز داریم، بنابراین محل واقعی ذخیره‌سازی داده‌ها اهمیت کمتری نسبت به رفتار مشابه با اشاره‌گر دارد.

نوع Box<T> در نهایت به‌صورت یک tuple struct با یک عضو تعریف شده است، بنابراین در لیست 15-8 نوع MyBox<T> را به همان شیوه تعریف می‌کنیم. همچنین تابعی با نام new تعریف خواهیم کرد تا با تابع new که روی Box<T> تعریف شده، مطابقت داشته باشد.

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}
Listing 15-8: تعریف نوع MyBox<T>

ما یک ساختار با نام MyBox تعریف می‌کنیم و یک پارامتر جنریک T اعلام می‌کنیم، زیرا می‌خواهیم نوع ما مقادیر از هر نوعی را نگه دارد. نوع MyBox یک ساختار tuple با یک عنصر از نوع T است. تابع MyBox::new یک پارامتر از نوع T می‌گیرد و یک نمونه از MyBox که مقدار ورودی را نگه می‌دارد برمی‌گرداند.

بیایید تابع main در لیستینگ ۱۵-۷ را به لیستینگ ۱۵-۸ اضافه کنیم و آن را برای استفاده از نوع MyBox<T> که تعریف کرده‌ایم، به جای Box<T> تغییر دهیم. کد موجود در لیستینگ ۱۵-۹ کامپایل نخواهد شد، زیرا Rust نمی‌داند چگونه MyBox را اشاره‌گر (Pointer)‌زدایی کند.

Filename: src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-9: تلاش برای استفاده از MyBox<T> به همان شیوه‌ای که از ارجاعات و Box<T> استفاده کردیم

در اینجا خطای کامپایل که نتیجه می‌شود:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error

نوع MyBox<T> ما نمی‌تواند اشاره‌گر (Pointer)‌زدایی شود زیرا ما این قابلیت را روی نوع خود پیاده‌سازی نکرده‌ایم. برای فعال کردن اشاره‌گر (Pointer)‌زدایی با عملگر *، ما ویژگی Deref را پیاده‌سازی می‌کنیم.

پیاده‌سازی Deref Trait

همان‌طور که در بخش «پیاده‌سازی یک Trait روی یک نوع» در فصل ۱۰ بحث شد، برای پیاده‌سازی یک trait باید پیاده‌سازی‌هایی برای متدهای موردنیاز آن trait ارائه دهیم. Trait به نام Deref که توسط کتابخانه استاندارد ارائه شده است، از ما می‌خواهد که یک متد به نام deref پیاده‌سازی کنیم که self را به‌صورت وام‌گرفته دریافت کرده و یک رفرنس به داده درونی بازمی‌گرداند. لیست 15-10 پیاده‌سازی‌ای از Deref را نشان می‌دهد که باید به تعریف MyBox<T> اضافه شود.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
Listing 15-10: پیاده‌سازی Deref روی MyBox<T>

سینتکس type Target = T; یک نوع مرتبط برای ویژگی Deref تعریف می‌کند تا از آن استفاده کند. نوع‌های مرتبط یک روش کمی متفاوت برای اعلام یک پارامتر جنریک هستند، اما نیازی نیست در حال حاضر نگران آن‌ها باشید؛ ما در فصل ۲۰ جزئیات بیشتری درباره آن‌ها ارائه خواهیم داد.

بدنه‌ی متد deref را با &self.0 پر می‌کنیم تا deref یک رفرنس به مقداری که می‌خواهیم با عملگر * به آن دسترسی پیدا کنیم، برگرداند؛ به یاد داشته باشید از بخش [«استفاده از tuple struct‌ها بدون فیلدهای نام‌دار برای ایجاد نوع‌های مختلف»][tuple-structs] در فصل ۵ که .0 به اولین مقدار در یک tuple struct دسترسی پیدا می‌کند. تابع main در لیست ۱۵-۹ که روی مقدار MyBox<T> عمل * را فراخوانی می‌کند اکنون کامپایل می‌شود و عبارت‌های assert نیز با موفقیت عبور می‌کنند!

بدون trait به نام Deref، کامپایلر فقط می‌تواند رفرنس‌های & را dereference کند. متد deref این امکان را به کامپایلر می‌دهد که بتواند یک مقدار از هر نوعی که Deref را پیاده‌سازی کرده بگیرد و متد deref را روی آن صدا بزند تا یک رفرنس & دریافت کند که بتواند آن را dereference کند.

وقتی که در لیستینگ ۱۵-۹ *y وارد کردیم، پشت صحنه Rust در واقع این کد را اجرا کرد:

*(y.deref())

Rust عملگر * را با یک فراخوانی به متد deref و سپس یک اشاره‌گر (Pointer)زدایی ساده جایگزین می‌کند، بنابراین لازم نیست درباره این فکر کنیم که آیا نیاز به فراخوانی متد deref داریم یا نه. این ویژگی Rust به ما اجازه می‌دهد کدی بنویسیم که خواه ارجاع معمولی باشد یا نوعی که Deref را پیاده‌سازی کرده باشد، به طور یکسان عمل کند.

دلیلی که متد deref یک رفرنس به یک مقدار بازمی‌گرداند، و این‌که هنوز هم نیاز داریم از عملگر dereference ساده خارج از پرانتزها در *(y.deref()) استفاده کنیم، به سیستم مالکیت مربوط می‌شود. اگر متد deref مقدار را به‌صورت مستقیم بازمی‌گرداند به‌جای بازگرداندن یک رفرنس به مقدار، آنگاه آن مقدار از self خارج (move) می‌شد. ما نمی‌خواهیم در این حالت، یا در بیشتر حالت‌هایی که از عملگر dereference استفاده می‌کنیم، مالکیت مقدار درونی در MyBox<T> را به‌دست بگیریم.

توجه داشته باشید که عملگر * با یک فراخوانی به متد deref و سپس یک فراخوانی به عملگر * فقط یک بار جایگزین می‌شود، هر بار که از * در کدمان استفاده می‌کنیم. از آنجایی که جایگزینی عملگر * بی‌نهایت تکرار نمی‌شود، در نهایت به داده‌ای از نوع i32 می‌رسیم که با 5 در assert_eq! در لیستینگ ۱۵-۹ مطابقت دارد.

فشار اشاره‌گر (Pointer)زدایی ضمنی با توابع و متدها

فشار اشاره‌گر (Pointer)زدایی (Deref coercion) یک ارجاع به نوعی که ویژگی Deref را پیاده‌سازی کرده است به یک ارجاع به نوعی دیگر تبدیل می‌کند. برای مثال، فشار اشاره‌گر (Pointer)زدایی می‌تواند &String را به &str تبدیل کند، زیرا String ویژگی Deref را به گونه‌ای پیاده‌سازی کرده است که &str بازمی‌گرداند. فشار اشاره‌گر (Pointer)زدایی یک ویژگی کاربردی در Rust است که روی آرگومان‌های توابع و متدها اعمال می‌شود و فقط روی انواعی که ویژگی Deref را پیاده‌سازی کرده‌اند عمل می‌کند. این ویژگی به‌صورت خودکار زمانی که یک ارجاع به مقدار یک نوع خاص به‌عنوان آرگومان به یک تابع یا متدی که نوع پارامتر آن با تعریف تابع یا متد مطابقت ندارد، اتفاق می‌افتد. یک توالی از فراخوانی‌های متد deref نوعی را که ارائه داده‌ایم به نوعی که پارامتر نیاز دارد تبدیل می‌کند.

فشار اشاره‌گر (Pointer)زدایی به Rust اضافه شد تا برنامه‌نویسانی که توابع و متدها را می‌نویسند نیاز نداشته باشند مرجع‌دهی‌ها و اشاره‌گر (Pointer)زدایی‌های واضح زیادی با & و * اضافه کنند. این ویژگی همچنین به ما امکان می‌دهد کدی بنویسیم که می‌تواند برای ارجاعات یا اشاره‌گر (Pointer)های هوشمند کار کند.

برای دیدن عملکرد تبدیل خودکار با استفاده از deref (deref coercion) در عمل، بیایید از نوع MyBox<T> که در لیستینگ 15-8 تعریف کردیم، همراه با پیاده‌سازی Deref که در لیستینگ 15-10 اضافه کردیم، استفاده کنیم. لیستینگ 15-11 تعریفی از یک تابع را نشان می‌دهد که یک پارامتر از نوع اسلایس رشته (&str) دارد.

Filename: src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}
Listing 15-11: یک تابع hello که پارامتر name از نوع &str دارد

می‌توانیم تابع hello را با یک اسلایس رشته به‌عنوان آرگومان فراخوانی کنیم، مانند hello("Rust"); برای مثال. فشار اشاره‌گر (Pointer)زدایی این امکان را فراهم می‌کند که hello را با یک ارجاع به یک مقدار از نوع MyBox<String> فراخوانی کنیم، همان‌طور که در لیستینگ ۱۵-۱۲ نشان داده شده است:

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}
Listing 15-12: فراخوانی hello با یک ارجاع به یک مقدار MyBox<String> که به دلیل فشار اشاره‌گر (Pointer)زدایی کار می‌کند

در اینجا ما تابع hello را با آرگومان &m که یک ارجاع به یک مقدار MyBox<String> است فراخوانی می‌کنیم. از آنجا که ما ویژگی Deref را روی MyBox<T> در لیستینگ ۱۵-۱۰ پیاده‌سازی کردیم، Rust می‌تواند &MyBox<String> را به &String با فراخوانی deref تبدیل کند. کتابخانه استاندارد پیاده‌سازی ویژگی Deref روی String را ارائه می‌دهد که یک اسلایس رشته بازمی‌گرداند، و این در مستندات API برای Deref ذکر شده است. Rust متد deref را دوباره فراخوانی می‌کند تا &String را به &str تبدیل کند که با تعریف تابع hello مطابقت دارد.

اگر Rust فشار اشاره‌گر (Pointer)زدایی را پیاده‌سازی نکرده بود، مجبور بودیم کدی مانند لیستینگ ۱۵-۱۳ را به‌جای کد لیستینگ ۱۵-۱۲ بنویسیم تا hello را با یک مقدار از نوع &MyBox<String> فراخوانی کنیم.

Filename: src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}
Listing 15-13: کدی که باید می‌نوشتیم اگر Rust فشار اشاره‌گر (Pointer)زدایی نداشت

عملگر (*m) مقدار MyBox<String> را به یک String اشاره‌گر (Pointer) زدایی می‌کند. سپس & و [..] یک برش رشته‌ای از String می‌گیرند که برابر با کل رشته است تا با امضای تابع hello تطابق داشته باشد. این کد بدون فشار اشاره‌گر (Pointer)زدایی با تمام این نمادها دشوارتر برای خواندن، نوشتن و درک است. فشار اشاره‌گر (Pointer)زدایی به Rust اجازه می‌دهد این تبدیل‌ها را به‌صورت خودکار برای ما انجام دهد.

وقتی ویژگی Deref برای انواع درگیر تعریف شود، Rust انواع را تحلیل می‌کند و از Deref::deref به دفعات لازم استفاده می‌کند تا یک ارجاع برای مطابقت با نوع پارامتر به دست آید. تعداد دفعاتی که نیاز به فراخوانی Deref::deref است در زمان کامپایل حل می‌شود، بنابراین هیچ هزینه‌ای در زمان اجرا برای استفاده از فشار اشاره‌گر (Pointer)زدایی وجود ندارد!

نحوه تعامل فشار اشاره‌گر (Pointer)زدایی با قابلیت تغییرپذیری

مشابه نحوه استفاده از ویژگی Deref برای بازنویسی عملگر * روی ارجاعات غیرقابل تغییر، می‌توانید از ویژگی DerefMut برای بازنویسی عملگر * روی ارجاعات قابل تغییر استفاده کنید.

Rust هنگام پیدا کردن انواع و پیاده‌سازی‌های ویژگی در سه حالت فشار اشاره‌گر (Pointer)زدایی را انجام می‌دهد:

۱. از &T به &U زمانی که T: Deref<Target=U> باشد ۲. از &mut T به &mut U زمانی که T: DerefMut<Target=U> باشد ۳. از &mut T به &U زمانی که T: Deref<Target=U> باشد

دو مورد اول مشابه یکدیگر هستند، با این تفاوت که مورد دوم، قابلیت تغییر (mutability) را نیز پیاده‌سازی می‌کند. مورد اول بیان می‌کند که اگر یک &T داشته باشید و T پیاده‌ساز Deref برای نوعی U باشد، می‌توانید به‌صورت شفاف (بدون نیاز به تبدیل دستی) یک &U دریافت کنید. مورد دوم نیز بیان می‌کند که همین تبدیل deref coercion برای رفرنس‌های قابل تغییر نیز اعمال می‌شود.

حالت سوم پیچیده‌تر است: Rust همچنین یک ارجاع قابل تغییر را به یک ارجاع غیرقابل تغییر تبدیل می‌کند. اما عکس آن ممکن نیست: ارجاعات غیرقابل تغییر هرگز به ارجاعات قابل تغییر تبدیل نمی‌شوند. به دلیل قوانین قرض‌گیری، اگر یک ارجاع قابل تغییر داشته باشید، آن ارجاع قابل تغییر باید تنها ارجاع به آن داده باشد (در غیر این صورت، برنامه کامپایل نمی‌شد). تبدیل یک ارجاع قابل تغییر به یک ارجاع غیرقابل تغییر هرگز قوانین قرض‌گیری را نمی‌شکند. تبدیل یک ارجاع غیرقابل تغییر به یک ارجاع قابل تغییر نیازمند این است که ارجاع غیرقابل تغییر اولیه تنها ارجاع غیرقابل تغییر به آن داده باشد، اما قوانین قرض‌گیری این را تضمین نمی‌کنند. بنابراین، Rust نمی‌تواند فرض کند که تبدیل یک ارجاع غیرقابل تغییر به یک ارجاع قابل تغییر امکان‌پذیر است.