دوستدار نرمافزار، فلسفه و ادبیات. وب سایت:http://www.alihoseiny.ir
آموزش زبان برنامهنویسی 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 با کلّی درد و خونریزی همراه خواهد بود.
چطوری به نتیجهی کدهایی که در این جلسه زدیم دسترسی داشته باشم؟
میتوانید تمام کارهایی که در این جلسه کردیم را یک جا ببینید.
اگر قسمت قبلی را نخواندهای، با کلیک روی این نوشته با Struct ها آشنا شو.
مطلبی دیگر از این انتشارات
کاتلین رفیق بی کلک اندروید
مطلبی دیگر از این انتشارات
آموزش زبان برنامهنویسی Rust - قسمت 1: شروع کار با متغیّرها و ثوابت
مطلبی دیگر از این انتشارات
ترفندهای RxJava در اندروید - قسمت اول