رفتار اشارهگر (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)زدایی برای دنبال کردن ارجاع به مقدار استفاده میکنیم:
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)
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>
در لیستینگ ۱۵-۷ استفاده شده است، به همان شیوهای عمل میکند که روی ارجاع در لیستینگ ۱۵-۶ عمل
میکرد:
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) هوشمند خودمان
بیایید یک اشارهگر (Pointer) هوشمند مشابه نوع Box<T>
که توسط کتابخانه استاندارد ارائه شده است بسازیم تا تجربه کنیم که
چگونه اشارهگر (Pointer)های هوشمند به طور پیشفرض متفاوت از ارجاعات رفتار میکنند. سپس به نحوه اضافه کردن قابلیت استفاده از
عملگر اشارهگر (Pointer)زدایی میپردازیم.
نوع Box<T>
در نهایت به عنوان یک ساختار tuple با یک عنصر تعریف شده است، بنابراین لیستینگ ۱۵-۸ نوع 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
همانطور که در بخش “پیادهسازی یک ویژگی روی یک نوع” فصل ۱۰ بحث شد، برای پیادهسازی یک
ویژگی، باید پیادهسازیهایی برای متدهای مورد نیاز ویژگی ارائه دهیم. ویژگی Deref
که توسط کتابخانه استاندارد
ارائه شده است، از ما میخواهد که یک متد به نام deref
را پیادهسازی کنیم که self
را قرض بگیرد و یک ارجاع به
داده داخلی بازگرداند. لیستینگ ۱۵-۱۰ شامل یک پیادهسازی از Deref
است که به تعریف MyBox
اضافه شده است:
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 بدون فیلدهای نامگذاریشده برای
ایجاد انواع مختلف” در فصل ۵ که .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
که در لیستینگ ۱۵-۱۰ اضافه کردیم استفاده کنیم. لیستینگ ۱۵-۱۱ تعریف یک تابع که یک پارامتر از نوع
اسلایس رشته دارد را نشان میدهد:
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>
باشد
دو حالت اول مشابه یکدیگر هستند با این تفاوت که حالت دوم قابلیت تغییرپذیری را پیادهسازی میکند. حالت اول
بیان میکند که اگر شما یک &T
داشته باشید و T
ویژگی Deref
را به نوعی U
پیادهسازی کند، میتوانید
بهصورت شفاف یک &U
دریافت کنید. حالت دوم بیان میکند که همین فشار اشارهگر (Pointer)زدایی برای ارجاعات قابل تغییر نیز
اتفاق میافتد.
حالت سوم پیچیدهتر است: Rust همچنین یک ارجاع قابل تغییر را به یک ارجاع غیرقابل تغییر تبدیل میکند. اما عکس آن ممکن نیست: ارجاعات غیرقابل تغییر هرگز به ارجاعات قابل تغییر تبدیل نمیشوند. به دلیل قوانین قرضگیری، اگر یک ارجاع قابل تغییر داشته باشید، آن ارجاع قابل تغییر باید تنها ارجاع به آن داده باشد (در غیر این صورت، برنامه کامپایل نمیشد). تبدیل یک ارجاع قابل تغییر به یک ارجاع غیرقابل تغییر هرگز قوانین قرضگیری را نمیشکند. تبدیل یک ارجاع غیرقابل تغییر به یک ارجاع قابل تغییر نیازمند این است که ارجاع غیرقابل تغییر اولیه تنها ارجاع غیرقابل تغییر به آن داده باشد، اما قوانین قرضگیری این را تضمین نمیکنند. بنابراین، Rust نمیتواند فرض کند که تبدیل یک ارجاع غیرقابل تغییر به یک ارجاع قابل تغییر امکانپذیر است.