وبلاگ شخصی
محمّدرضا
علی حسینی

جایی که تجربیات, علایق
و چیزهایی که یادگرفته‌ام را
با هم مرور می‌کنیم.

آموزش زبان برنامه‌نویسی Rust - قسمت ۱۷: نگاهی خیلی دقیق‌تر به ویژگی‌ها

آموزش زبان برنامه‌نویسی 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 در rust

یعنی مثلاً ما در 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);

حالا این کد بدون مشکل اجرا می‌شود.

خب این هم از این قسمت. تقریباً الان همه‌چیز را درمورد ویژگی‌ها می‌دانیم. چند تا قابلیّت خاص هنوز باقی مانده‌اند که بعداً سر فرصت به آن‌ها هم رسیدگی می‌کنیم.

در قسمت بعدی با هم Generic ها را شروع می‌کنیم تا شیوه‌ی دوم پیاده‌سازی چندریختی (Polymorphism) را هم در Rust یادبگیریم.

دریافت کدهای این قسمت

برای دریافت کدهای این قسمت، می‌توانید با کلیک روی این نوشته به مخزن این مجموعه‌ی آموزشی در گیت‌هاب بروید.

در قسمت قبلی با خصوصیّت‌های پرکاربرد ویژگی‌ها آشنا شدیم. اگر آن قسمت را از دست داده ای، با کلیک روی این نوشته خیلی سریع آن را هم بخوان.

دفعه‌ی اوّلی است که این مجموعه‌ی آموزشی را می‌بینی؟ روی این نوشته کلیک کن و آموزش Rust را از اوّل شروع کن.

«نوشته‌های مرتبط»

6 پاسخ به “آموزش زبان برنامه‌نویسی Rust – قسمت ۱۷: نگاهی خیلی دقیق‌تر به ویژگی‌ها”

  1. احمد حسینی گفت:

    خسته نباشید بسیار عالی بود ، منتظر ادامه اش هستیم

    • محمّدرضا علی حسینی گفت:

      ممنون احمد عزیز. به زودی قسمت‌های بعدی را هم می‌نویسم. خوشحالم که نظرت را جلب کرده است.

  2. یارو گفت:

    خسته نباشی
    خیلی عالی بود
    دیگه دستم به کد زدن با زبون های دیگه نمیره 😐

    اگه یه پروزه کوچیک وب هم به صورت ویدئویی ضبط کنی خیلی عالی میشه

  3. یارو گفت:

    خیلی عالی بودن زنگوله رو هم زدم هر وقت قسمت بعدیش اومد نوتیفش بیاد
    فقط اگه ممکنه خیلی روی چیزهای مشترکی که توی همه زبونها هست وقت نذاری عالی تر میشه
    بیصبرانه منتظر قسمت های بعدیم

    • محمّدرضا علی حسینی گفت:

      سلام
      خیلی ممنون. حتماً تلاش می‌کنم به جز مواردی که به فهم همان مطلب کمک می‌کنند چیز دیگری را توضیح ندهم، ولی خب گاهی اوقات از دستم در‌می‌رود.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»