متد
متدها شبیه به توابع هستند: آنها را با کلیدواژهی fn و یک نام تعریف میکنیم،
میتوانند پارامتر و مقدار بازگشتی داشته باشند، و شامل مقداری کد هستند
که هنگام فراخوانی متد از جایی دیگر اجرا میشود. برخلاف توابع،
متدها در بستر یک struct (یا یک enum یا یک trait object که به ترتیب در فصل ۶ و فصل ۱۸ بررسی میشوند) تعریف میشوند،
و اولین پارامتر آنها همیشه self است، که نشاندهندهی نمونهای از struct است
که متد روی آن فراخوانی شده است.
تعریف متدها
بیایید تابع 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 بپردازیم تا ابزار دیگری به جعبه ابزار شما اضافه کنیم.