آموزش زبان برنامه‌نویسی Rust – قسمت۱۱- افزودن Method و Associated Function به Struct ها

در قسمت قبلی یادگرفتیم که struct چیست، چطور می‌توان آن را تعریف کرد و نحوه‌ی استفاده از آن چگونه است.

حالا می‌خواهیم به جنبه‌ی دیگر struct بپردازیم. الان می‌خواهیم علاوه بر کنار هم قرار دادن داده‌های مرتبط، عملکردهای مربوط به آن‌ها را هم درون struct جا بدهیم.

متد چیست؟

متد یا Method ها مثل تابع‌ها هستند. مانند آن‌ها با کلمه‌ی کلیدی fn تعریف می‌شوند، یک سری پارامتر ورودی می‌گیرند و می‌توانند خروجی داشته باشند.

امّا متدها تفاوت‌هایی هم با توابع معمولی دارند. آن‌ها باید در جای مخصوصی تعریف شوند و حتماً باید یک پارامتر به‌خصوص را داشته باشند.

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

خب دیگر بهتر است که برویم سراغ کد و دست از کلّی‌گویی برداریم.

نحوه‌ی تعریف یک Method در زبان Rust

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

ما باید method ها را درون بلاک پیاده‌سازی تعریف کنیم. چیزی که تا امروز مشابهش را ندیده‌ایم. بیایید اصلاً با یک مثال جلو برویم.

تغییر برنامه‌ی قبلی برای استفاده از Method

ساختار Student را از قسمت قبلی به یاد دارید؟

#[derive(Debug)]
struct Student {
    name: String,
    id: u32,
    graduated: bool
}

بیایید برای اینکه مثالمان با معنی شود یک ساختار دیگر هم تعریف کنیم:

#[derive(Debug)]
struct Course {
    name: String,
    passed: bool
}

ساختار Course قرار است اطّلاعات یک درس را نگهداری کند. برای سادگی هر درس فقط دو مقدار دارد: نام و passed که مشخّص می‌کند دانشجو در این درس قبول شده است یا نه.

حالا می‌خواهیم struct قبلی را که اطّلاعات دانشجو را ذخیره می‌کرد تغییر بدهیم تا اطّلاعات دروسی را که دانشجو آن‌ها را برداشته است نگهداری کند.

#[derive(Debug)]
struct Student {
    name: String,
    id: u32,
    courses: [Course; 3]
}

یک فیلد جدید با کلید courses به Student اضافه‌کرده‌ایم که یک آرایه‌ی ۳تایی را از struct جدیدمان، Course، نگهداری می‌کند.

به علاوه فیلد graduate را که قرار بود وضعیّت فارغ‌التّحصیل بودن یا نبودن دانشجو را از روی آن تشخیص بدهیم حذف کردیم. چون می‌خواهیم این مقدار را حالا با استفاده از یک method به دست بیاوریم.

نکته: در زبان Rust اگر قرار است فیلدی از نوع آرایه درون struct داشته باشیم، باید اندازه و نوع آن کاملاً مشخّص باشد. یعنی نمی‌توان از Dynamic Array برای این کار استفاده کرد (البته این موضوع که ما اصلاً در rust Dynamic Array نداریم هم در این موضوع بی‌تأثیر نیست).

تعریف Method

خب حالا می‌خواهیم یک Method برای Student تعریف کنیم. این method قرار است بررسی کند که آیا مقدار passed تمامی دروسی که دانشجو برداشته برابر با true است یا نه؟ اگر دانشجو تمامی درس‌هایش را پاس کرده بود می‌تواند فارغ‌التّحصیل بشود.

خب بیایید method نهایی را ببینیم و بعد با بررسی خط به خط آن شیوه‌ی تعریف یک method را یادبگیریم:

impl Student {
    fn check_graduation(&self) -> bool {
        for course in self.courses.iter() {
            if !course.passed {
                return false;
            }
        }
        return true;
    }
}

ما برای افزودن متدها به struct باید آن‌ها را درون بلاک impl قرار بدهیم. مقابل کلمه‌ی کلیدی impl باید نام struct مورد نظر که قرار است عملکرد (functionality) های آن را مشخّص کنیم، آورده شود.

نکته: برای یک struct می‌توان چندین بلاک impl داشت.

تعریف متد به شکل تعریف تابع است. ما ابتدا با کلمه‌ی کلیدی fn تعریف یک متد را شروع می‌کنیم. سپس اسم متد را می‌نویسیم. آخر سر هم درون پرانتز پارامترهای ورودی متد را مشخّص می‌کنیم.

هر متد یک پارامتر به نام self دارد. این self تا حدودی شبیه همان self متدهای کلاس‌های پایتون است.

self همان نمونه (instance) ای است که از struct ساخته‌ایم. با استفاده از آن می‌توانیم به خود struct و داده‌های موجود در فیلدهای آن دسترسی داشته باشیم.

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

بدنه‌ی method هم دقیقاً مثل بدنه‌ی تابع است.

کاری که ما در این متد کرده‌ایم این است که با فراخوانی iter روی آرایه‌ی درس‌ها، روی مقدار courses ساختار Student چرخیده‌ایم. اگر مقدار passed یکی از درس‌ها false باشد، یعنی اینکه دانشجو هنوز آن درس را پاس نکرده است. بنابراین نمی‌تواند فارغ‌التّحصیل بشود. به همین خاطر مقدار false را برمی‌گردانیم.

در انتها اگر تمامی درس‌ها پاس شده باشند، مقدار true را برمی‌گردانیم.

حالا بیایید برنامه را به شکل کلّی ببینیم:

#[derive(Debug)]
struct Student {
    name: String,
    id: u32,
    courses: [Course; 3]
}

#[derive(Debug)]
struct Course {
    name: String,
    passed: bool
}

impl Student {
    fn check_graduation(&self) -> bool {
        for course in self.courses.iter() {
            if !course.passed {
                return false;
            }
        }
        return true;
    }
}

fn main() {
    let course1 = Course {
        name: String::from("مهندسی نرم افزار"),
        passed: true
    };
    let course2 = Course {
        name: String::from("طراحی پایگاه داده"),
        passed: true
    };
    let course3 = Course {
        name: String::from("روش های رسمی در مهندسی نرم افزار"),
        passed: true
    };
    let asghar = Student {
        name: String::from("اصغر اکبرآبادی اصل"),
        id: 96532147,
        courses: [course1, course2, course3]
    };
    println!("Checking asghar graduation result is: {}", asghar.check_graduation())
}

حالا این برنامه را اجرا می‌کنیم تا ببینیم نتیجه‌اش چی می‌شود:

Checking asghar graduation result is: true

بچه‌ها، بچه‌ها، تبریک می‌گم. اصغر بالأخره فارغ‌التّحصیل شد.

استفاده از self با گرفتن مالکیّت یا بدون آن؟

همانطوری که از بحث مربوط به مالکیّت به خاطر دارید، اگر یک مقدار را به عنوان یک پارامتر به یک تابع بدهیم، مالکیّت آن را هم به آن تابع منتقل می‌کنیم.

متدها هم دقیقاً مثل توابع اند. بنابراین اگر self را به همین شکل به عنوان پارامتر متد تعریف کنیم، مالکیّت نمونه‌ی ساخته شده از struct را هم موقع فراخوانی متد به آن منتقل می‌کنیم.

مثلاً فرض‌کنید که متد check_graduation را مثل کد زیر تغییر بدهیم:

impl Student {
    fn check_graduation(self) -> bool {
        for course in self.courses.iter() {
            if !course.passed {
                return false;
            }
        }
        return true;
    }
}

همانطوری که می‌بینید این بار به جای اینکه یک رفرنس به self را به عنوان ورودی مشخص کنیم، خود آن را به عنوان پارامتر ورودی نوشته ایم.

حالا بیایید برنامه را با این تغییر جدید اجرا کنیم:

fn main() {
    let course1 = Course {
        name: String::from("مهندسی نرم افزار"),
        passed: true
    };
    let course2 = Course {
        name: String::from("طراحی پایگاه داده"),
        passed: true
    };
    let course3 = Course {
        name: String::from("روش های رسمی در مهندسی نرم افزار"),
        passed: true
    };
    let asghar = Student {
        name: String::from("اصغر اکبرآبادی اصل"),
        id: 96532147,
        courses: [course1, course2, course3]
    };
    println!("Checking asghar graduation result is: {}", asghar.check_graduation());
    print!("Asghar: {:#?}", asghar);
}

اگر این را کامپایل کنیم چه اتّفاقی می‌افتد؟

error[E0382]: use of moved value: `asghar`
  --> src/main.rs:50:29
   |
49 |     println!("Checking asghar graduation result is: {}", asghar.check_graduation());
   |                                                          ------ value moved here
50 |     print!("Asghar: {:#?}", asghar);
   |                             ^^^^^^ value used here after move
   |
   = note: move occurs because `asghar` has type `Student`, which does not implement the `Copy` trait

همانطوری که می‌بینید، وقتی که متد check_graduation را فراخوانی کرده‌ایم مالکیّت مقدار struct به متد منتقل شده است. پس هنگامی که دوباره می‌خواهیم از همان متغیّر asghar استفاده کنیم کد ما به ارور می‌خورد.

این self دقیقاً چیست؟

شاید برای کار با self کمی گیج شده باشید. برای همین بیایید یکم دقیق‌تر ببینیم که این self چیست.

مقدار self خود نمونه‌ی ساخته شده از ساختار است.

برای روشن شدن منظورم بیایید با هم یک مثال ساده را ببینم.

یک نمونه از ساختار Course می‌سازیم:

fn main() {
    let course_instance = Course {
        name: String::from("یک اسم الکی"),
        passed: false
    };
    println!("Course in the main function: {:#?}", course_instance);
}

خب طبیعتاً با اجرای این برنامه، اطّلاعات ساختار course_instance را که نمونه‌ای از struct قبلی ما با نام Course است در کنسول می‌بینیم:

Course in the main function: Course {
    name: "یک اسم الکی",
    passed: false
}

حالا می‌خواهیم که یک متد برای Course بنویسیم که هروقت فراخوانی می‌شود، محتویات ساختار را پرینت کند:

impl Course {
    fn print(&self) {
        println!("Course in the print method: {:#?}", self);
    }
}

همانطوری که می‌بیند متد ما خیلی ساده است. یک خط کد که در آن مقدار self را پرینت می‌کنیم تا ببینیم توی آن چیست.

حالا برنامه را کمی تغییر می‌دهیم تا بتوانیم درونش این متد را هم فراخوانی کنیم:

‍‍fn main() {
    let course_instance = Course {
        name: String::from("یک اسم الکی"),
        passed: false
    };
    println!("Course in the main function: {:#?}", course_instance);
    course_instance.print();
}

حالا این برنامه‌را اجرا می‌کنیم تا ببینیم که تفاوت متغیّر course_instance با پارامتر self آن چیست:

Course in the main function: Course {
    name: "یک اسم الکی",
    passed: false
}
Course in the print method: Course {
    name: "یک اسم الکی",
    passed: false
}

دیدید؟ هر دو تا دقیقاً مثل هم هستند. در حقیقت ما با کمک self می‌توانیم به همان نمونه (instance) ساخته شده از strucمان دسترسی داشته باشیم.

تغییر محتوای یک struct با استفاده از متدها

حالا اگر بخواهیم یک مقدار را درون struct عوض کنیم باید چه کار کنیم؟

همانطوری که خیلی زیاد تا اینجا دیدیم، متدها مثل توابع اند. به همین خاطر درست همانطوری که می‌توانستیم با ارسال یک رفرنس mutable به عنوان پارامتر به تابع مقدار آن را عوض کنیم، می‌توانیم اینجا هم مقدار struct را تغییر بدهیم.

تنها تفاوتش در این است که اینجا آن چیزی که باید یک رفرنس mutable از آن را جدا کنیم و به متد بفرستیم همان self است.

مثلاً‌ فرض کنید که می‌خواهیم یک متد بنویسیم که مقدار passed ساختار Course را true کند. یعنی متدی که با فراخوانی‌اش اعلام می‌کنیم که دانشجو توانسته است این درس را پاس کند:

impl Course {
    fn pass(&mut self) {
        self.passed = true;
    }
}

حالا می‌خواهیم ببینم که مقدار درس پیش و پس از فراخوانی این متد چه تغییری می‌کند:

fn main() {
    let mut course_instance = Course {
        name: String::from("یک اسم الکی"),
        passed: false
    };
    course_instance.print();
    course_instance.pass();
    course_instance.print();
}

حالا یک نفس عمیق می‌کشیم و با کسب اجازه از بزرگترهای مجلس برنامه را کامپایل و اجرا می‌کنیم:

Course in the print method: Course {
    name: "یک اسم الکی",
    passed: false
}
Course in the print method: Course {
    name: "یک اسم الکی",
    passed: true
}

همانطوری که انتظارش را داشتیم، مقدار passed ساختارمان پس از فراخوانی این متد تغییر کرد.

نکته: برای اجرای متدهایی که نیاز به تغییر بخشی از struct دارند، باید نمونه‌ای که از آن struct می‌سازیم به عنوان یک متغیّر mutable تعریف بشود. اگر mut را هنگام تعریف نمونه فراموش کنیم به ارور می‌خوریم.

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

چرا باید از Method ها استفاده کنیم؟

استفاده از متدها کمک می‌کند که برنامه‌ی ما ساختار بهتری پیداکند. وقتی که عملکرد (functionality) های مرتبط به یک ساختار داده به خود آن پیوند می‌خورد، نگهداری و توسعه‌ی برنامه راحت‌تر می‌شود.

این کار حتّی فعّالیّت‌هایی مثل نوشتن داکیومنت را هم برای ما ساده‌تر می‌کند.

ما با این کار عملاً یک namespace مخصوص برای عملکردهای مختص به یک struct ایجاد می‌کنیم.

Associated Function چیست؟

توابع مرتبط یا به قول ملّت فرنگ Associated Function ها تابع اند (از کرامات شیخ ما این است…).

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

حالا چرا به این توابع مرتبط می‌گویند؟ چون همچنان این تابع‌ها به یک struct خاص مرتبط اند. تحت نام آن کار می‌کنند و به صورت منطقی عملکردی مرتبط با آن struct دارند.

Associated function ها کاربردهای زیادی می‌توانند داشته باشند. رایج‌ترین کاربرد این توابع ساخت نمونه‌ای از یک struct است.

تا الان بارها و بارها از String::from استفاده کرده‌ایم. from یک تابع مرتبط به ساختار String است. وقتی که از آن استفاده می‌کنیم یک نمونه از ساختار String را به ما بر می‌گرداند (تعجّب کردید؟ خب String هم خودش یک strut است. بعداً خیلی عمیق و با جزئیات به حضرت String خواهیم پرداخت).

چطوری در زبان Rust یک Associated Function تعریف کنیم؟

حالا می‌خواهیم اوّلین associate function مان را در Rust بنویسیم.

آن factory functionی که در جلسه‌ی پیش برای ساخت نمونه‌هایی از ساختار Student ساخته بودیم را به خاطر دارید؟

حالا می‌خواهیم همان کار را این بار با یک تابع مرتبط به نام construct انجام بدهیم:

impl Student {
    fn construct(name: String, id: u32, courses: [Course; 3]) -> Student{
    	Student {
        name,
        id,
        courses,
	    }
	}
}

حالا می‌خواهیم با استفاده از این associate function یک نمونه از ساختار Student درست کنیم:

fn main() {
    let course1 = Course {
        name: String::from("مهندسی نرم افزار"),
        passed: true
    };
    let course2 = Course {
        name: String::from("طراحی پایگاه داده"),
        passed: true
    };
    let course3 = Course {
        name: String::from("روش های رسمی در مهندسی نرم افزار"),
        passed: true
    };
    let asghar = Student::construct(String::from("اصغر اکبرآبادی اصل"), 96532147, [course1, course2, course3]);
    println!("Asghar revived with an associated function: {:#?}", asghar);
}

همانطوری که می‌بینید برای استفاده از یک associate function کافی است اسم struct مربوطه را بنویسیم و بعد از گذاشتن :: نام تابع را بنویسیم.

حالا اگر این برنامه‌را کامپایل و اجرا کنیم می‌بینیم که اصغر دوباره به زندگی برگشته است:

Asghar revived with an associated function: Student {
    name: "اصغر اکبرآبادی اصل",
    id: 96532147,
    courses: [
        Course {
            name: "مهندسی نرم افزار",
            passed: true
        },
        Course {
            name: "طراحی پایگاه داده",
            passed: true
        },
        Course {
            name: "روش های رسمی در مهندسی نرم افزار",
            passed: true
        }
    ]
}

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

در قسمت بعدی به خطاهایی که ممکن است هنگام کار با struct به آن‌ها بخوریم و مباحث باقی‌مانده درمورد نحوه‌ی ذخیره‌سازی آن‌ها در حافظه و… خواهیم پرداخت.

فرق Rust با دیگر زبان‌ها در ساخت struct دارای functionality چیست؟

حالا که فهمیدیم که چطوری می‌توان در Rust به یک struct با استفاده از method یا associate function عملکرد اضافه‌کرد، ببینیم تفاوت Rust با بقیه‌ی زبان‌ها در چیست.

شما فرض‌کنید که می‌خواهید همین ساختار Course را با متد pass در زبان C پیاده‌سازی کنید.

اوّل کد را ببینیم:

#include <stdio.h>
struct Course {
	char *name;
	short int passed;
	void (*pass) (struct Course * c);
};

void pass(struct Course *c) {
	c->passed = 1;
}

int main() {
	struct Course course;
	course.name = "We can not use Persian Letters simply";
	course.passed = 0;
	course.pass = pass;	// Assigning pass function to pass value of the struct
	course.pass(&course);	// We should send instance of the struct to the function expressly
	if (course.passed) {
		printf("Worked. But we can not print the whole struct simply like what we did in rust.");
	} else {
		printf("Didn't work");
	}
	return 0;
}

ما ابتدا یک struct تعریف کرده‌ایم. مسئله‌ی اول این است که در c هیچ امکان پیش‌فرضی برای اینکه عملکردها را به یک struct متّصل کنیم وجود ندارد. به همین خاطر ما مجبوریم یک پوینتر به یک تابع‌را به عنوان یکی از اعضای struct تعریف کنیم. یعنی عملاً داریم یکجورهایی زبان را دور می‌زنیم.

اسم این پوینتر را pass می‌گذاریم و ورودی‌اش را یک پوینتر به یک نمونه از همین struct قرار می‌دهیم.

این پوینتر قرار است جای همان self را بگیرد. چیزی که در Rust خود کامپایلر زحمت تعریف نوع و … را برایش می‌کشید.

حالا باید تابع خودمان را تعریف کنیم. ما اینجا نام تابع را pass گذاشتیم، امّا این نام می‌تواند هر چیزی باشد و تابع می‌تواند هرجایی استفاده بشود.

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

فرض‌کنید که از این تابع علاوه بر این struct در یک جای دیگر برنامه هم استفاده شده است. حالا کسی که آن بخش از برنامه را نگهداری می‌کند تابع را عوض می‌کند. اینطوری خیلی ساده تمامی بخش‌هایی که از struct شما استفاده می‌کنند نتیجه‌ی غلط خواهند داشت.

حالا ما تابع را نوشتیم، امّا هنوز این تابع هیچ ربطی به مقدار pass درون struct ندارد. برای اینکه این دو تا به هم مرتبط بشوند، باید جایی که داریم نمونه‌ای از struct می‌سازیم، در اینجا درون تابع main، خودمان به صورت دستی بگوییم که تابع pass را به مقدار pass این struct ارجاع بده.

خب حالا در یک برنامه‌ی بزرگ با تعداد زیادی struct تصوّر کنید که چقدر خطا از همین کار بیرون می‌آید. هزاران بار فراموش می‌کنید که تابع را به مقدار مشابهش در struct متّصل کنید و این یعنی هزاران بار به خطا می‌خورید.

آخر سر هم وقتی که می‌خواهیم از متد استفاده کنیم باید خودمان یک پوینتر به همان مقدار درست کنیم و به عنوان ورودی به متد بدهیم.

تازه در c راهی برای پرینت کردن تمام یک struct ندارید. این هم خودش یک مشکل دیگر.

می‌بینید؟ کاری که در Rust به راحتی و بدون خطا انجام می‌شود در c با کلّی درد و خون‌ریزی همراه خواهد بود.

چطوری به نتیجه‌ی کدهایی که در این جلسه زدیم دسترسی داشته باشم؟

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

می‌توانید تمام کارهایی که در این جلسه کردیم را یک جا ببینید.

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

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

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

خواندن قسمت بعدی روی ویرگول