رفتار با اشارهگرهای هوشمند مانند رفرنسهای معمولی با استفاده از 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)زدایی برای دنبال کردن ارجاع به مقدار استفاده میکنیم:
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
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>
در لیستینگ ۱۵-۷ استفاده شده است، به همان شیوهای عمل میکند که روی ارجاع در لیستینگ ۱۵-۶ عمل
میکرد:
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
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>
تعریف شده، مطابقت داشته باشد.
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
MyBox<T>
ما یک ساختار با نام MyBox
تعریف میکنیم و یک پارامتر جنریک T
اعلام میکنیم، زیرا میخواهیم نوع ما مقادیر
از هر نوعی را نگه دارد. نوع MyBox
یک ساختار tuple با یک عنصر از نوع T
است. تابع MyBox::new
یک پارامتر از نوع
T
میگیرد و یک نمونه از MyBox
که مقدار ورودی را نگه میدارد برمیگرداند.
بیایید تابع main
در لیستینگ ۱۵-۷ را به لیستینگ ۱۵-۸ اضافه کنیم و آن را برای استفاده از نوع MyBox<T>
که
تعریف کردهایم، به جای Box<T>
تغییر دهیم. کد موجود در لیستینگ ۱۵-۹ کامپایل نخواهد شد، زیرا Rust نمیداند
چگونه MyBox
را اشارهگر (Pointer)زدایی کند.
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);
}
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>
اضافه شود.
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); }
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
) دارد.
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
hello
که پارامتر name
از نوع &str
داردمیتوانیم تابع hello
را با یک اسلایس رشته بهعنوان آرگومان فراخوانی کنیم، مانند hello("Rust");
برای مثال.
فشار اشارهگر (Pointer)زدایی این امکان را فراهم میکند که hello
را با یک ارجاع به یک مقدار از نوع MyBox<String>
فراخوانی کنیم، همانطور که در لیستینگ ۱۵-۱۲ نشان داده شده است:
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); }
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>
فراخوانی کنیم.
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)[..]); }
عملگر (*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 نمیتواند فرض کند که تبدیل یک ارجاع غیرقابل تغییر به یک ارجاع قابل تغییر امکانپذیر است.