آموزش زبان برنامهنویسی Rust - قسمت ۱۷: نگاهی خیلی دقیقتر به ویژگیها
در ۲ قسمت قبل، ما اوّل فهمیدیم که ویژگی یا همان trait چیست و چرا چندریختی (polymorphism) مهم است. بعد از آن نگاهی دقیقتر به ویژگیها انداختیم و چیزهای مختلفی مثل متدهای پیشفرض یا ارثبری بین ویژگیها را یادگرفتیم.
حالا در این قسمت میخواهیم با هم نگاهی دقیقتر از قبل به ویژگیها بیندازیم و چند کاربرد اختصاصیتر آنها را هم یادبگیریم.
فهرست مطالب
توابع مرتبط
ما قبلاً دیدیم که در ساختارها (struct) توابع مرتبط یا associtated function ها چی هستند. برای یک یادآوری سریع، توابع مرتبط، توابعی هستند که به یک ساختار مرتبط هستند و تنها از طریق آن ساختار میتوان به آنها دسترسی داشت، امّا یک نمونه (instance) از آن ساختار را به عنوان ورودی نمیگیرند.
عملاً میتوان توابع مرتبط را با static method در زبانهای شئگرا یکی درنظرگرفت.
ما میتوانیم در ویژگیها هم توابع مرتبط را تعریف کنیم. اینطوری هر نوعدادهای که ویژگی ما را پیادهسازی میکند، باید آن تابع مرتبط را هم پیادهسازی کند.
مثلاً فرضکنید که ما میخواهیم هر دادهای که ویژگی Fly
را دارد، یک constructor هم داشته باشد.
کافی است ویژگی Fly
را به شکل زیر تغییر بدهیم:
trait Fly { fn new() -> Self; fn fly(&self); fn land(&self) { println!("Flyable Object now landing."); } }
حالا موقع پیادهسازی Fly
برای نوع دادهی Kaftar
، باید تابع مرتبط new
را هم پیادهسازی کنیم. تابع new
همان constructor انواعی است که ویژگی Fly
را پیادهسازی میکنند. یعنی قرار است با صدازدن آن یک نمونه (instance) از دادهای که میخواهیم بسازیم. برای همین نوع خروجی این تابع Self
است.
حالا بیایید برنامهی قسمتهای قبلیمان را با استفاده از این تابع جدید از نو بنویسیم:
trait Fly { fn new() -> Self; fn fly(&self); fn land(&self) { println!("Flyable Object now landing."); } } struct Kaftar (); impl Fly for Kaftar { fn new() -> Self { return Kaftar{}; } fn fly(&self) { println!("Kafter The Kakol Be Sar is flying"); } } fn main() { let kakol_be_sar = Kaftar::new(); kakol_be_sar.fly(); }
اگر برنامه را اجرا کنیم همان خروجی قبلی را دریافت میکنیم:
Kafter The Kakol Be Sar is flying
همانطوری که میبینید دقیقاً مثل چیزی که برای struct ها و enum ها دیدیم، برای فراخوانی توابع مرتبط، اسم ساختار را مینویسیم و سپس بعد از علامت ::
نام تابع را مینویسیم.
فرق self با Self چیست؟
همانطوری که میبینید ما موقعی که میخواستیم بگوییم نوع خروجی تابع new
همان نوع دادهای است که ویژگی Fly
برایش پیادهسازی شده است، از کلمهی کلیدی Self
استفاده کردیم. امّا موقعی که میخواستیم مشخّص کنیم که اوّلین ورودی متد
ما همان نمونهای از داده است که داریم این متد را روی آن فراخوانی میکنیم، از کلمهی کلیدی self
استفاده کردیم.
حالا تفاوت Self
با self
در چیست؟
Self
همان نوع شئ کنونی است. مثلاً وقتی که داریم برای ساختار Kaftar
ویژگی Fly
را پیادهسازی میکنیم، Self
همان Kaftar
است. امّا وقتی که داریم این ویژگی را برای ساختار Airplane
پیادهسازی میکنیم، منظور از Self
نوع Airplane
خواهد بود.
حالا ما در متدها نیازداریم که به خود شئای که دارد این متد را فراخوانی میکند هم دسترسی داشته باشیم. به همین خاطر اوّلین پارامتر ورودی آن را مقدار self
، این بار با s کوچک، میگذاریم.
فرق این self
با آن Self
چیست؟ در حقیقت self
یک سینتکس خلاصه برای همان Self
است.
در جدول زیر شیوههای مختلف استفاده از self
و معنی آنها را میبینیم:
حالت خلاصه | معنی آن برای کامپایلر |
---|---|
self | self: Self |
self& | self: &Self |
mut self& | self: &mut Self |
Fly
به جای اینکه بنویسیم:
fn fly(&self);
میتوانیم بنویسیم:
fn fly(self: &Self);
این دو شکل تعریف متد fly
دقیقاً یکسان اند. ما صرفاً هنگام مشخّص کردن پارامترهای ورودی متدها از self
استفاده میکنیم چون کوتاهتر است.
هزار و یک روش برای فراخوانی یک متد
ما همیشه برای فراخوانی متدها خیلی ساده عمل میکنیم. اوّل اسم شئ را مینویسیم. بعد علامت .
را میگذاریم و بعد از آن هم اسم متد را مینویسیم. این کاری است که احتمالاً تا پایان عمرتان هم برای فراخوانی متدها خواهید کرد.
امّا میتوان یک متد را به روشهای مختلفی فراخوانی کرد.
حالت سادهای که همین الان یادآوریش کردیم اینطوری است:
kaftar.fly();
یک روش دیگر این است که که اوّل اسم خود ساختار را بنویسیم و بعد از علامت ::
نام متد را بیاوریم:
let kakol_be_sar = Kaftar{}; Kaftar::fly(&kakol_be_sar);
ما اوّل یک نمونه از نوع Kaftar
تعریف میکنیم. سپس برای اینکه متد fly
را روی آن فراخوانی کنیم، ابتدا نام ساختاری که این نمونه از آن ساخته شده است را مینویسیم. حالا بعد از ::
نام متدی را که میخواهیم آن را صدا بزنیم میآوریم.
موقعی که از این روش برای صدازدن یک متد استفاده میکنیم باید دقیقاً تمام ورودیهایی که متد میگیرد را به آن بدهیم. اوّلین ورودی هر متد هم self
است. حالا چون ما در این متد یک رفرنس به self
را به عنوان ورودی اوّل میگرفتیم، پس اینجا هم یک رفرنس از شئای که ساختهایم را موقع فراخوانی fly
به آن میفرستیم.
اگر متد علاوه بر self ورودیهای دیگری هم بگیرد میتوانیم آنها را بعد از شئ موردنظرمان بنویسیم.
یک روش دیگر این است که به جای فراخوانی متد روی ساختار و پاسدادن شئ به آن، متد را روی خود ویژگی فراخوانی کنیم. یعنی برای فراخوانی متد fly
میتوانیم این کار را هم بکنیم:
let kakol_be_sar = Kaftar{}; Fly::fly(&kakol_be_sar);
برای اینکه بعداً اگر شنیدید یا در منابع انگلیسی خواندید تعجّب نکنید، به جز روش اوّل، به روشهای دیگر اصطلاحاً qualified method call میگویند.
حالا چرا باید به جای اینکه مثل آدم بعد از اسم شئ یک .
بگذاریم و اسم متد را بنویسیم، باید از این روشها استفاده کنیم؟
انتخاب بین چند متد همنام
حالت اوّلی که ممکن است نیازشود از این روشها استفاده کنیم، این است که ساختار ما دو ویژگی (trait) مختلف را پیادهسازی کرده باشد که در هر دو یک متد با یک نام وجود داشته باشد.
مثلاً فرضکنید که ما دو تا ویژگی زیر را داریم:
trait Brush { fn draw(&self); fn change_colour(&self); } trait Screen { fn draw(&self); fn turnoff(&self); }
اوّلین ویژگی، قلممو بودن! است. اگر متد draw
را فراخوانی کنید قلممو یک خط روی کاغذ میکشد. دومین ویژگی هم صفحهنمایش بودن است. وقتی که متد draw
را فراخوانی کنید یک خط روی صفحهی نمایش کشیده میشود (بله. بله. خیلی مثال احمقانهای است امّا سادهترین چیزی است که به ذهنم رسید).
فرضکنید که ما یک ساختار به نام Something
داریم که هر دوی این ویژگیها را پیادهسازی میکند:
struct Something(); impl Brush for Something { fn draw(&self) { println!("Draw a line on the paper."); } fn change_colour(&self) { println!("Brush colour changed."); } } impl Screen for Something { fn draw(&self) { println!("Draw a line on the screen"); } fn turnoff(&self) { println!("Screen turned off"); } }
حالا میخواهیم یک نمونه از Something
بسازیم و متد draw
را فراخوانی کنیم:
fn main() { let something = Something{}; something.draw(); }
اگر برنامه را اجرا کنیم کدام یک از draw
ها فراخوانی میشوند؟
error[E0034]: multiple applicable items in scope --> src/main.rs:66:15 | 66 | something.draw(); | ^^^^ multiple `draw` found | note: candidate #1 is defined in an impl of the trait `Brush` for the type `Something` --> src/main.rs:45:5 | 45 | fn draw(&self) { | ^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl of the trait `Screen` for the type `Something` --> src/main.rs:55:5 | 55 | fn draw(&self) { | ^^^^^^^^^^^^^^
هیچکدام. کامپایلر Rust نمیتواند بفهمد که منظور از draw
کدام یک از draw
ها است. به همین خاطر نمیتواند برنامه را به درستی کامپایل کند.
اگر بخواهیم که این مشکل را رفع کنیم، میتوانیم از روش سوم برای فراخوانی متد draw
استفاده کنیم:
fn main() { let something = Something{}; Brush::draw(&something); Screen::draw(&something); }
حالا میتوانیم با خیال راحت این برنامه را اجرا کنیم:
Draw a line on the paper. Draw a line on the screen
فقط حواستان باشد که اگر این مشکل در کد خودتان به وجود آمده است، احتمالاً تغییر دادن اسم متدها راهکار بهتری است. ولی وقتهایی که داریم از کدهای دیگران استفاده میکنیم این امکان وجود ندارد.
فراخوانی متدها وقتی که نوع داده قابل تشخیص نیست
حالت دیگری که ممکن است رخ بدهد این است که کامپایلر نتواند نوع داده را تشخیص بدهد تا بفهمد که کدام متد را باید فراخوانی کند.
کد زیر را در نظر بگیرید:
let x = -10; x.abs(x);
الان نوع x
چه چیزی است؟ ممکن است x
هر یک از انواع: i8
، i16
، i32
یا i64
باشد. هر کدام از این نوعها هم abs
مخصوص به خودشان را پیادهسازی کردهاند. به همین خاطر وقتی میگوییم که abs
را صدا کن، کامپایلر نمیتواند بفهمد که منظورمان کدام abs
است.
میتوانیم با روش دوم این مشکل را برطرف کنیم:
let x = -10; i32::abs(x);
حالا این کد بدون مشکل اجرا میشود.
The form you have selected does not exist.
خب این هم از این قسمت. تقریباً الان همهچیز را درمورد ویژگیها میدانیم. چند تا قابلیّت خاص هنوز باقی ماندهاند که بعداً سر فرصت به آنها هم رسیدگی میکنیم.
در قسمت بعدی با هم Generic ها را شروع میکنیم تا شیوهی دوم پیادهسازی چندریختی (Polymorphism) را هم در Rust یادبگیریم.
دریافت کدهای این قسمت
یا حرفهای شو یا برنامهنویسی را رها کن.
چطور میشود در بازار کار خالی از نیروی سنیور ایران، بدون کمک دیگران و وقت تلفکردن خودت را تبدیل به یک نیروی باتجربه بکنی؟
پاسخ ساده است. باید حرفهای بشوی. چیزی که من در این کتاب به تو یاد خواهم داد.
خیلی عالی بودن زنگوله رو هم زدم هر وقت قسمت بعدیش اومد نوتیفش بیاد
فقط اگه ممکنه خیلی روی چیزهای مشترکی که توی همه زبونها هست وقت نذاری عالی تر میشه
بیصبرانه منتظر قسمت های بعدیم
عاااااااالی.
دارم پله پله راست نویس میشم.
واقعا دمت گرم که این همه وقت گذاشتی برای این آموزش.
یه اشکال کوچیک تو همین صفحه!
«اگر بخواهیم که این مشکل را رفع کنیم، میتوانیم از روش سوم برای فراخوانی متد draw استفاده کنیم» فکر کنم باید مینوشتی روش دوم.