متد
متدها شبیه به توابع هستند: ما آنها را با کلمه کلیدی fn
و یک نام تعریف میکنیم، میتوانند پارامترها و یک مقدار بازگشتی داشته باشند و شامل کدی هستند که وقتی متد از جایی دیگر فراخوانی میشود، اجرا میشود. برخلاف توابع، متدها در زمینه یک ساختار (یا یک Enum یا یک Trait Object، که آنها را به ترتیب در فصل ۶ و فصل ۱۷ پوشش میدهیم) تعریف میشوند و پارامتر اول آنها همیشه self
است که نمونهای از ساختاری که متد روی آن فراخوانی شده است را نمایش میدهد.
تعریف متدها
بیایید تابع area
که یک نمونه از Rectangle
را به عنوان پارامتر میگیرد، تغییر دهیم و به جای آن، یک متد area
تعریف کنیم که روی ساختار Rectangle
تعریف شده است، همانطور که در لیست ۵-۱۳ نشان داده شده است.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
area
روی ساختار Rectangle
برای تعریف تابع در زمینه Rectangle
، یک بلوک impl
(پیادهسازی) برای Rectangle
شروع میکنیم. هر چیزی در این بلوک impl
با نوع Rectangle
مرتبط خواهد بود. سپس، تابع area
را به درون آکولادهای impl
منتقل کرده و اولین (و در اینجا تنها) پارامتر آن را در امضا و در هر جایی در بدنه به self
تغییر میدهیم. در main
، جایی که تابع area
را فراخوانی میکردیم و rect1
را به عنوان آرگومان ارسال میکردیم، اکنون میتوانیم از نحو متد برای فراخوانی متد area
روی نمونه Rectangle
خود استفاده کنیم. نحو متد بعد از یک نمونه قرار میگیرد: نقطهای اضافه میکنیم و به دنبال آن نام متد، پرانتزها و هر آرگومان دیگری قرار میدهیم.
در امضای area
، از &self
به جای rectangle: &Rectangle
استفاده میکنیم. &self
در واقع معادل کوتاهشدهای از self: &Self
است. درون یک بلوک impl
، نوع Self
نام مستعاری برای نوعی است که بلوک impl
برای آن تعریف شده است. متدها باید به عنوان پارامتر اول خود یک پارامتری به نام self
از نوع Self
داشته باشند، بنابراین Rust به شما اجازه میدهد این عبارت را با فقط نوشتن self
در محل اولین پارامتر کوتاه کنید. توجه داشته باشید که همچنان باید از &
در مقابل اختصار self
استفاده کنیم تا نشان دهیم که این متد نمونه Self
را قرض میگیرد، دقیقاً همانطور که در rectangle: &Rectangle
استفاده میکردیم. متدها میتوانند مالکیت self
را بگیرند، self
را به صورت غیرقابل تغییر قرض بگیرند، همانطور که در اینجا انجام دادهایم، یا self
را به صورت قابل تغییر قرض بگیرند، دقیقاً مانند هر پارامتر دیگری.
ما در اینجا &self
را انتخاب کردهایم به همان دلیلی که در نسخه تابع از &Rectangle
استفاده کردیم: ما نمیخواهیم مالکیت را بگیریم و فقط میخواهیم دادهها را در ساختار بخوانیم، نه اینکه آنها را تغییر دهیم. اگر بخواهیم نمونهای که متد روی آن فراخوانی شده است را به عنوان بخشی از کاری که متد انجام میدهد تغییر دهیم، به عنوان پارامتر اول از &mut self
استفاده میکنیم. داشتن متدی که مالکیت نمونه را میگیرد با استفاده از فقط self
به عنوان پارامتر اول به ندرت اتفاق میافتد؛ این تکنیک معمولاً زمانی استفاده میشود که متد self
را به چیز دیگری تبدیل کند و شما بخواهید از استفاده از نمونه اصلی پس از تبدیل جلوگیری کنید.
دلیل اصلی استفاده از متدها به جای توابع، علاوه بر ارائه نحو متد و عدم نیاز به تکرار نوع self
در امضای هر متد، سازماندهی است. ما تمام کارهایی که میتوانیم با یک نمونه از یک نوع انجام دهیم را در یک بلوک impl
قرار دادهایم، به جای اینکه کاربران آینده کد ما به دنبال قابلیتهای Rectangle
در مکانهای مختلف در کتابخانهای که ارائه میدهیم بگردند.
توجه داشته باشید که میتوانیم تصمیم بگیریم متدی با همان نام یک فیلد ساختار تعریف کنیم. برای مثال، میتوانیم متدی روی Rectangle
تعریف کنیم که نام آن نیز width
باشد:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
Here is the continuation of the translation for “ch05-03-method-syntax.md” into Persian:
در اینجا ما تصمیم گرفتهایم متد width
را طوری تعریف کنیم که اگر مقدار در فیلد width
نمونه بزرگتر از 0
باشد مقدار true
و در غیر این صورت مقدار false
برگرداند: ما میتوانیم از یک فیلد درون یک متد با همان نام برای هر منظوری استفاده کنیم. در main
، وقتی که ما rect1.width
را با پرانتز دنبال میکنیم، Rust میداند که منظور ما متد width
است. وقتی از پرانتز استفاده نمیکنیم، Rust میداند که منظور ما فیلد width
است.
اغلب، اما نه همیشه، زمانی که به یک متد نامی مشابه یک فیلد میدهیم، میخواهیم که این متد تنها مقدار موجود در فیلد را بازگرداند و هیچ کار دیگری انجام ندهد. متدهایی مانند اینها getter نامیده میشوند، و Rust آنها را به صورت خودکار برای فیلدهای ساختار پیادهسازی نمیکند، همانطور که برخی از زبانهای دیگر انجام میدهند. Getterها مفید هستند زیرا میتوانید فیلد را خصوصی کنید اما متد را عمومی کنید و به این ترتیب دسترسی فقط-خواندنی به آن فیلد را به عنوان بخشی از API عمومی نوع فعال کنید. ما در فصل ۷ در مورد عمومی و خصوصی بودن و چگونگی تعیین عمومی یا خصوصی بودن یک فیلد یا متد بحث خواهیم کرد.
کجاست عملگر ->
؟
در C و C++، دو عملگر مختلف برای فراخوانی متدها استفاده میشود: شما از .
استفاده میکنید اگر متد را روی خود شیء فراخوانی میکنید و از ->
اگر متد را روی یک اشارهگر (Pointer) به شیء فراخوانی میکنید و نیاز دارید ابتدا اشارهگر (Pointer) را اشارهبرداری کنید. به عبارت دیگر، اگر object
یک اشارهگر (Pointer) باشد، object->something()
شبیه به (*object).something()
است.
Rust معادل عملگر ->
را ندارد؛ به جای آن، Rust یک ویژگی به نام ارجاعدهی و اشارهبرداری خودکار دارد. فراخوانی متدها یکی از معدود مکانهایی در Rust است که این رفتار را دارد.
اینگونه کار میکند: وقتی یک متد را با object.something()
فراخوانی میکنید، Rust به طور خودکار &
، &mut
یا *
را اضافه میکند تا object
با امضای متد مطابقت داشته باشد. به عبارت دیگر، موارد زیر یکسان هستند:
#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
اولین مورد خیلی تمیزتر به نظر میرسد. این رفتار ارجاعدهی خودکار کار میکند زیرا متدها یک گیرنده واضح دارند—نوع self
. با توجه به گیرنده و نام یک متد، Rust میتواند به طور قطعی تعیین کند که آیا متد در حال خواندن (&self
)، تغییر (&mut self
) یا مصرف (self
) است. این واقعیت که Rust قرضگیری را برای گیرندههای متد ضمنی میکند، بخش بزرگی از راحتی کار با مالکیت در عمل است.
متدهایی با پارامترهای بیشتر
بیایید با تعریف یک متد دیگر روی ساختار Rectangle
تمرین کنیم. این بار میخواهیم یک نمونه از Rectangle
نمونه دیگری از Rectangle
را بگیرد و مقدار true
برگرداند اگر Rectangle
دوم کاملاً در self
(اولین Rectangle
) جای گیرد؛ در غیر این صورت مقدار false
برگرداند. به عبارت دیگر، پس از تعریف متد can_hold
، میخواهیم بتوانیم برنامهای بنویسیم که در لیست ۵-۱۴ نشان داده شده است.
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold
که هنوز نوشته نشده استخروجی مورد انتظار به صورت زیر خواهد بود زیرا هر دو بُعد rect2
کوچکتر از ابعاد rect1
هستند، اما rect3
از rect1
عریضتر است:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
ما میدانیم که میخواهیم یک متد تعریف کنیم، بنابراین این متد در بلوک impl Rectangle
خواهد بود. نام متد can_hold
خواهد بود و یک قرض غیرقابل تغییر از یک Rectangle
دیگر به عنوان پارامتر خواهد گرفت. میتوانیم نوع پارامتر را با نگاه به کدی که متد را فراخوانی میکند تشخیص دهیم: rect1.can_hold(&rect2)
مقدار &rect2
را ارسال میکند، که یک قرض غیرقابل تغییر به rect2
، یک نمونه از Rectangle
است. این منطقی است زیرا ما فقط نیاز به خواندن rect2
داریم (نه نوشتن، که به یک قرض قابل تغییر نیاز داشت) و میخواهیم مالکیت rect2
در main
باقی بماند تا بتوانیم پس از فراخوانی متد can_hold
دوباره از آن استفاده کنیم. مقدار بازگشتی can_hold
یک مقدار بولی خواهد بود و پیادهسازی بررسی میکند که آیا عرض و ارتفاع self
به ترتیب بزرگتر از عرض و ارتفاع Rectangle
دیگر هستند. بیایید متد جدید can_hold
را به بلوک impl
از لیست ۵-۱۳ اضافه کنیم، همانطور که در لیست ۵-۱۵ نشان داده شده است.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
can_hold
روی Rectangle
که یک نمونه دیگر از Rectangle
را به عنوان پارامتر میگیردHere is the continuation of the translation for “ch05-03-method-syntax.md” into Persian:
وقتی این کد را با تابع main
موجود در لیست ۵-۱۴ اجرا میکنیم، خروجی دلخواه را دریافت خواهیم کرد. متدها میتوانند چندین پارامتر بگیرند که ما آنها را پس از پارامتر self
به امضا اضافه میکنیم، و این پارامترها همانند پارامترهای توابع عمل میکنند.
توابع مرتبط
تمام توابعی که در یک بلوک impl
تعریف شدهاند توابع مرتبط نامیده میشوند، زیرا با نوعی که بعد از impl
نامگذاری شده است، مرتبط هستند. ما میتوانیم توابع مرتبطی را تعریف کنیم که self
را به عنوان اولین پارامتر خود ندارند (و بنابراین متد نیستند) زیرا نیازی به کار با یک نمونه از نوع ندارند. ما قبلاً از یک تابع مشابه استفاده کردهایم: تابع String::from
که روی نوع String
تعریف شده است.
توابع مرتبطی که متد نیستند اغلب برای سازندهها استفاده میشوند که نمونه جدیدی از ساختار را بازمیگردانند. این توابع معمولاً new
نامیده میشوند، اما new
یک نام خاص نیست و در زبان به صورت داخلی تعریف نشده است. برای مثال، ما میتوانیم تصمیم بگیریم تابع مرتبطی به نام square
ارائه دهیم که یک پارامتر برای ابعاد بگیرد و از آن به عنوان عرض و ارتفاع استفاده کند، بنابراین ایجاد یک Rectangle
مربعی را آسانتر میکند به جای اینکه مجبور باشیم مقدار یکسان را دو بار مشخص کنیم:
Filename: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
کلمات کلیدی Self
در نوع بازگشتی و در بدنه تابع، نام مستعاری برای نوعی هستند که بعد از کلمه کلیدی impl
ظاهر میشود، که در اینجا Rectangle
است.
برای فراخوانی این تابع مرتبط، از نحو ::
همراه با نام ساختار استفاده میکنیم؛ برای مثال: let sq = Rectangle::square(3);
. این تابع با ساختار فضای نامگذاری شده است: نحو ::
برای توابع مرتبط و فضای نامهای ایجاد شده توسط ماژولها استفاده میشود. ما ماژولها را در فصل ۷ بررسی خواهیم کرد.
بلوکهای متعدد impl
هر ساختار اجازه دارد چندین بلوک impl
داشته باشد. برای مثال، لیست ۵-۱۵ معادل کدی است که در لیست ۵-۱۶ نشان داده شده است، که هر متد در بلوک impl
خود قرار دارد.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
impl
هیچ دلیلی برای جدا کردن این متدها به بلوکهای متعدد impl
در اینجا وجود ندارد، اما این یک نحو معتبر است. ما در فصل ۱۰ موردی را خواهیم دید که در آن بلوکهای متعدد impl
مفید هستند، جایی که ما نوعهای عمومی و ویژگیها را بررسی خواهیم کرد.
خلاصه
ساختارها به شما اجازه میدهند تا نوعهای سفارشی ایجاد کنید که برای حوزه کاری شما معنادار باشند. با استفاده از ساختارها، میتوانید قطعات دادهای مرتبط را به هم متصل کنید و برای هر قطعه نامی تعیین کنید تا کد شما شفاف شود. در بلوکهای impl
، شما میتوانید توابعی را تعریف کنید که با نوع شما مرتبط هستند، و متدها نوعی از توابع مرتبط هستند که به شما اجازه میدهند رفتار نمونههای ساختارهایتان را مشخص کنید.
اما ساختارها تنها راه ایجاد نوعهای سفارشی نیستند: بیایید به ویژگی Enum در Rust بپردازیم تا ابزار دیگری به جعبه ابزار شما اضافه کنیم.