رفتار اشاره‌گر (Pointer)های هوشمند مانند ارجاعات معمولی با استفاده از ویژگی 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)
help: consider dereferencing here
 --> file:///home/.rustup/toolchains/1.82/lib/rustlib/src/rust/library/core/src/macros/mod.rs:46:35
  |
46|                 if !(*left_val == **right_val) {
  |                                   +

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) هوشمند خودمان

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

نوع Box<T> در نهایت به عنوان یک ساختار tuple با یک عنصر تعریف شده است، بنابراین لیستینگ ۱۵-۸ نوع 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

همان‌طور که در بخش “پیاده‌سازی یک ویژگی روی یک نوع” فصل ۱۰ بحث شد، برای پیاده‌سازی یک ویژگی، باید پیاده‌سازی‌هایی برای متدهای مورد نیاز ویژگی ارائه دهیم. ویژگی Deref که توسط کتابخانه استاندارد ارائه شده است، از ما می‌خواهد که یک متد به نام deref را پیاده‌سازی کنیم که self را قرض بگیرد و یک ارجاع به داده داخلی بازگرداند. لیستینگ ۱۵-۱۰ شامل یک پیاده‌سازی از Deref است که به تعریف MyBox اضافه شده است:

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 بدون فیلدهای نام‌گذاری‌شده برای ایجاد انواع مختلف” در فصل ۵ که .0 به اولین مقدار در یک ساختار tuple دسترسی پیدا می‌کند. تابع main در لیستینگ ۱۵-۹ که * را روی مقدار MyBox<T> فراخوانی می‌کند اکنون کامپایل می‌شود و تاییدها موفق خواهند شد!

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

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

*(y.deref())

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

دلیل اینکه متد deref یک ارجاع به مقدار بازمی‌گرداند و اشاره‌گر (Pointer)زدایی ساده در بیرون از پرانتز در *(y.deref()) همچنان لازم است، به سیستم مالکیت مرتبط است. اگر متد deref به‌جای یک ارجاع به مقدار، مقدار را مستقیماً بازمی‌گرداند، مقدار از self منتقل می‌شد. در این حالت یا در بیشتر مواردی که از عملگر اشاره‌گر (Pointer)زدایی استفاده می‌کنیم، نمی‌خواهیم مالکیت مقدار داخلی درون 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)های هوشمند کار کند.

برای دیدن فشار اشاره‌گر (Pointer)زدایی در عمل، بیایید از نوع MyBox<T> که در لیستینگ ۱۵-۸ تعریف کردیم به همراه پیاده‌سازی Deref که در لیستینگ ۱۵-۱۰ اضافه کردیم استفاده کنیم. لیستینگ ۱۵-۱۱ تعریف یک تابع که یک پارامتر از نوع اسلایس رشته دارد را نشان می‌دهد:

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> باشد

دو حالت اول مشابه یکدیگر هستند با این تفاوت که حالت دوم قابلیت تغییرپذیری را پیاده‌سازی می‌کند. حالت اول بیان می‌کند که اگر شما یک &T داشته باشید و T ویژگی Deref را به نوعی U پیاده‌سازی کند، می‌توانید به‌صورت شفاف یک &U دریافت کنید. حالت دوم بیان می‌کند که همین فشار اشاره‌گر (Pointer)زدایی برای ارجاعات قابل تغییر نیز اتفاق می‌افتد.

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