مقدمه

یک متد (method) مثل تابع، با استفاده از کلمه کلیدی fn تعریف می‌شود، می‌تواند دارای یک یا چند پارامتر باشد و مقداری را برگرداند. در واقع، یک متد نوعی تابع است که به یک ساختار (struct) یا همانطور که بعداً خواهیم دید، به یک enum یا یک trait object مربوط است. اولین پارامتر یک متد همیشه self است که به نمونه‌ای که متد را فراخوانی کرده، اشاره می‌کند. در این درس، پروژه‌ی rectangles را که در درس قبل ایجاد شد، طوری ویرایش می‌کنیم که تابع area() به عنوان یک متدِ ساختار Rectangle تعریف شود.

تعریف متد

در آخرین نسخه از پروژه‌ی rectangles به کدی رسیدیم که در آن، تابع area() دارای یک پارامتر از نوع Rectangle بود. حالا این کد را طوری تغییر می‌دهیم که area() یک متدِ ساختارِ Rectangle باشد.

Copy Icon src/main.rs
#[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()
  );
}

برای اینکه یک تابع را به عنوان متدی از ساختار Rectangle تعریف کنیم، از یک بلاک impl برای نوع Rectangle استفاده شده است. هر چیزی که درون این بلاک تعریف شود، متعلق به نوع Rectangle خواهد بود. سپس، تابع area() را به بلاک impl منتقل کرده و اولین و در اینحا تنها پارامتر آن را self نامیده‌ایم. در متد main() هم به جای اینکه یک نمونه‌ی Rectangle را به تابع area() پاس کنیم، با استفاده از method syntax، متد area() را روی نمونه‌ی مورد نظر فراخوانی کرده‌ایم. حالا self به این نمونه (یعنی rect1) اشاره می‌کند. پس، method syntax یا سینتکس فراخوانی متد به این صورت است که بعد از نام نمونه‌ی مورد نظر یک کاراکتر dot و سپس نام متد آورده می‌شود.

در تعریف متد area() از &self به جای rectangle: &Rectangle استفاده شده است. در واقع، &self اختصاری است برای self: &Self و Self در یک بلاک impl نام مستعاری است برای نوعی که impl به آن مربوط است. اولین پارامتر یک متد، الزاماً باید پارامتری با نام self از نوع Self باشد. Rust اجازه می‌دهد این پارامتر را به جای self: Self فقط با نوشتن self ایجاد کنیم. توجه داشته باشید که اگر بخواهیم این پارامتر، یک رفرنس باشد، باید از &self استفاده کنیم. بنابراین، در مجموع می‌توان گفت:

  • پارامتر self معادل self: Self است.
  • پارامتر &self معادل self: &Self است.

متدها می‌توانند مالکیت self را دریافت کنند یا یک رفرنس immutable یا یک رفرنس mutable به آن ایجاد کنند. ما در اینجا به همان دلیلی که در تابع area() نوع پارامتر را &Rectangle تعیین کردیم، در متد area() پارامتر را به صورت &self تعیین کرده‌ایم. یعنی چون نمی‌خواهیم مالکیت نمونه را دریافت کنیم، یک رفرنس به آن ایجاد کرده‌ایم و چون نمی‌خوهیم دیتای آن را دستکاری کنیم و فقط می‌خواهیم آن را بخوانیم، این رفرنس به صورت immutable تعریف شده است. اگر بخواهیم یک رفرنس mutable به نمونه ایجاد کنیم، باید پارامتر اول متد را به صورت &mut self بنویسیم. اما اینکه متدی تعریف کنیم که مالکیت نمونه را بگیرد، یعنی پارامتر اول آن به صورت self باشد، اتفاق نادری است و معمولاً فقط زمانی از این تکنیک استفاده می‌شود که قرار باشد متد ما self را به چیز دیگری تبدیل کند و بخواهیم بعد از تبدیل، نمونه‌ی اورجینال در دسترس نباشد.

استفاده از متدها به جای توابع، علاوه بر راحتی استفاده از سینتکس متد و عدم نیاز به تکرار نوع self در هر فراخوانی، یک مزیت ویژه‌ی دیگر هم دارد و آن سازماندهی کدهاست. ما می‌توانیم هر کاری را که می‌توان با یک نمونه از یک نوع انجام داد، در یک بلاک impl قرار دهیم و به این ترتیب، هم خودمان و هم افراد دیگری که ممکن است گذرشان به کد ما بیفتد، می‌دانند که کجا باید دنبال قابلیت‌های مربوط به یک نوع مثل Rectangle بگردند.

امکان تعریف متدهای همنام با فیلدهای یک ساختار هم وجود دارد. برای مثال، می‌توانیم متدی برای نوع Rectangle تعریف کنیم که نامش width باشد.

Copy Icon src/main.rs
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);
  }
}

متد width() را طوری تعریف کرده‌ایم که اگر مقدار فیلد width برای نمونه (instance) بزرگتر از صفر باشد، مقدار true را برگرداند و اگر برابر با صفر باشد، مقدار false را. تعریف متدهای همنام با فیلدها می‌تواند به دلایل مختلفی انجام شود اما معمولاً وقتی متدی را همنام با یک فیلد تعریف می‌کنیم، می‌خواهیم که این متد فقط مقدار فیلد همنامش را برگرداند و بس. این دست متدها را getter می‌نامند و باید بدانید که Rust بر خلاف خیلی از زبان‌های دیگر، متدهای getter را به صورت خودکار برای فیلدهای ساختارها پیاده‌سازی نکرده است. متدهای getter به این دلیل مفید هستند که به ما امکان می‌دهند که یک فیلد را به صورت private تعریف کنیم و متد را به صورت public و به این ترتیب، امکان یک دسترسی read-only به فیلد را فراهم کنیم. در فصل هفتم در مورد این موضوع و نحوه‌ی انجام آن صحبت خواهیم کرد.

متدهای دارای پارامترهای بیشتر

اجازه دهید با تعریف یک متد دیگر روی ساختار Rectangle، کار با متدها را بیشتر تمرین کنیم. متدی که می‌خواهیم تعریف کنیم، can_hold() نام دارد و کاری که باید انجام دهد این است که یک نمونه‌ی Rectangle را دریافت کند و آن را با self (یعنی نمونه‌ای که متد روی آن فراخوانی می‌شود) مقایسه کند و اگر هر دو فیلد width و height برای این نمونه از مقدار width و height مربوط به self کوچکتر باشند، مقدار true را برگرداند و در غیر این صورت، مقدار false را. این متد را به صورت زیر تعریف می‌کنیم.

Copy Icon src/main.rs
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
  }
}

متد can_hold() علاوه بر self یک پارامتر دیگر هم دارد که این یکی هم از نوع Rectangle است. این پارامتر other نامگذاری شده و در بدنه‌ی متد یک عبارت وجود دارد که شرط بزرگتر بودن فیلدهای self از فیلدهای other را چک می‌کند. طبیعتاً اگر این شرط برقرار باشد، مقدار true و اگر برقرار نباشد، مقدار false توسط متد برگردانده می‌شود. در ضمن، نوع پارامتر other هم یک رفرنس immutable است؛ چون ما نمی‌خواهیم که متد مالکیت مقدار این پارامتر را دریافت کند و قصد ویرایش آن را هم نداریم. حالا از این متد در تابع main() به صورت زیر استفاده می‌کنیم.

Copy Icon src/main.rs
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 rect1 hold rect2? true
Can rect1 hold rect3? false
          

پس، متدها می‌توانند علاوه بر self، پارامترهای دیگری هم داشته باشند و این پارامترها درست مثل پارامترهای توابع رفتار می‌کنند.

توابع Associated

هر تابعی که درون یک بلاک impl تعریف شود، یک تابع associated است؛ چون چنین تابعی به نوعی که بعد از کلمه کلیدی impl می‌آید، مرتبط (associated) است. اما بعضی از این توابع، متد نیستند؛ یعنی پارامتر اول آنها self نیست. علت اینکه می‌توانیم چنین توابعی داشته باشیم این است که برخی توابع نیازی به یک نمونه که روی آن فراخوانی شوند، ندارند. در واقع، این توابع نه در سطح نمونه (instance-level) بلکه در سطح نوع (type-level) هستند. در زبان‌های دیگر این توابع را استاتیک می‌نامند. یک نمونه از این توابع که قبلاً هم با آن کار کرده‌ایم، تابع String::from() است.

یک تابع associated که متد نباشد، معمولاً نقش یک سازنده (constructor) را دارد؛ یعنی یک نمونه‌ی جدید از یک ساختار را برمی گرداند. این توابع را معمولاً new نامگذاری می‌کنیم اما توجه داشته باشید که new در Rust معنای خاصی ندارد و بخشی از زبان Rust نیست.

برای مثال، می‌توانیم یک تابع associated با نام square() برای ساختار Rectangle تعریف کنیم که یک پارامتر دریافت کند و آن را هم به عنوان مقدار فیلد width و هم به عنوان فیلد height در نظر بگیرد و به این ترتیب، یک مستطیل با عرض و ارتفاع یکسان (یک مربع) برگرداند.

Copy Icon src/main.rs
impl Rectangle {
  fn square(size: u32) -> Self {
    Self {
        width: size,
        height: size,
    }
  }
}

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

let square = Rectangle::square(3);