Методи
Методи подібні до функцій: Ми оголошуємо їх за допомогою ключового слова fn і
імені, вони можуть мати параметри та значення, що повертається, і вони містять код,
який виконується, коли метод викликається звідкись іще. На відміну від функцій,
методи визначаються в контексті структури (або перелічення (enum), або об’єкта трейту (trait object),
що ми розглядаємо в Розділі 6 та Розділі
18 відповідно), і їхній перший параметр завжди
self, який представляє екземпляр структури, для якого викликається метод.
Синтаксис методів
Давайте змінимо функцію area, яка має екземпляр Rectangle як параметр,
і натомість зробимо методом area, визначеним для структури Rectangle, як показано
в Лістингу 5-13.
#[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. Усе в межах цього блоку impl
буде пов’язане з типом Rectangle. Потім ми переміщуємо функцію area
всередину фігурних дужок impl і змінюємо перший (і в цьому випадку єдиний)
параметр на self у сигнатурі та всюди в тілі. У main, де ми викликали
функцію area і передавали rect1 як аргумент,
ми можемо натомість використати синтаксис методів, щоб викликати метод area на нашому
екземплярі Rectangle. Синтаксис методів іде після екземпляра: Ми додаємо крапку, за якою
йде ім’я методу, круглі дужки та будь-які аргументи.
У сигнатурі для area ми використовуємо &self замість rectangle: &Rectangle.
&self насправді є скороченням для self: &Self. Усередині блоку impl
тип Self є псевдонімом для типу, для якого призначений блок impl. Методи повинні
мати параметр на ім’я self типу Self як свій перший параметр, тому Rust
дозволяє скоротити це, використовуючи лише ім’я self у першій позиції параметра.
Зверніть увагу, що нам усе ще потрібно використовувати & перед скороченням self, щоб
позначити, що цей метод запозичує екземпляр Self, так само як ми робили в
rectangle: &Rectangle. Методи можуть брати володіння на self, запозичувати self
незмінно, як ми зробили тут, або запозичувати self змінно, так само як і будь-який
інший параметр.
Ми вибрали тут &self з тієї ж причини, з якої використовували &Rectangle у
версії функції: Ми не хочемо брати володіння, і ми лише хочемо читати дані в
структурі, а не записувати в них. Якби ми хотіли змінити екземпляр, на якому ми
викликали метод, як частину того, що робить метод, ми б використали &mut self як
перший параметр. Мати метод, який бере володіння на екземпляр, використовуючи лише
self як перший параметр, трапляється рідко; цю техніку зазвичай
використовують, коли метод перетворює self на щось інше, і ви хочете
заборонити викликачу використовувати оригінальний екземпляр після перетворення.
Головна причина використовувати методи замість функцій, окрім
надання синтаксису методів і відсутності потреби повторювати тип self у сигнатурі кожного
методу, — це організація. Ми зібрали все, що можемо робити
з екземпляром типу, в одному блоці impl, а не змушуємо майбутніх користувачів
нашого коду шукати можливості Rectangle у різних місцях у
бібліотеці, яку ми надаємо.
Зверніть увагу, що ми можемо дати методу те саме ім’я, що й одному з полів структури.
Наприклад, ми можемо визначити метод для Rectangle, який також називається width:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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 повертав true, якщо значення в
полі width екземпляра більше за 0, і false, якщо значення дорівнює
0: Ми можемо використовувати поле всередині методу з тим самим іменем для будь-якої мети. У
main, коли ми додаємо круглі дужки після rect1.width, Rust знає, що ми маємо на увазі
метод width. Коли ми не використовуємо круглі дужки, Rust знає, що ми маємо на увазі поле
width.
Часто, але не завжди, коли ми даємо методу те саме ім’я, що й поле, ми хочемо, щоб він лише повертав значення поля і більше нічого не робив. Такі методи називаються гетерами, і Rust не реалізує їх автоматично для полів структури, як це роблять деякі інші мови. Гетери корисні, тому що ви можете зробити поле приватним, але метод — публічним і таким чином надати доступ лише для читання до цього поля як частини публічного API типу. Ми обговоримо, що таке публічне і приватне та як позначити поле або метод як публічний чи приватний у Розділі 7.
Де оператор
->?У C і C++, для виклику методів використовуються два різні оператори: Ви використовуєте
.якщо викликаєте метод безпосередньо на об’єкті, і->якщо ви викликаєте метод на вказівнику на об’єкт і спочатку потрібно розіменувати вказівник. Іншими словами, якщоobject— це вказівник,object->something()подібне до(*object).something().У Rust немає еквівалента оператору
->; натомість у Rust є можливість, яка називається автоматичне посилання та розіменування. Виклик методів — одне з небагатьох місць у Rust із такою поведінкою.Ось як це працює: Коли ви викликаєте метод за допомогою
object.something(), Rust автоматично додає&,&mutабо*, щобobjectвідповідав сигнатурі методу. Іншими словами, наведене нижче є однаковим:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }Перше виглядає значно охайніше. Така автоматична поведінка посилання працює тому, що методи мають чіткий одержувач — тип
self. Маючи одержувача та ім’я методу, Rust може безпомилково визначити, чи метод читає (&self), змінює (&mut self) або споживає (self). Той факт, що Rust робить запозичення неявним для одержувачів методів, — велика частина того, що робить володіння ергономічною на практиці.
Методи з більшою кількістю параметрів
Давайте попрактикуємося у використанні методів, реалізувавши другий метод для структури Rectangle.
Цього разу ми хочемо, щоб екземпляр Rectangle приймав інший екземпляр
Rectangle і повертав true, якщо другий Rectangle може повністю поміститися
всередині self (першого Rectangle); інакше він повинен повертати false.
Тобто, після того як ми визначимо метод can_hold, ми хочемо мати змогу написати
програму, показану в Лістингу 5-14.
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));
}
Очікуваний вивід виглядав би так, як наведено нижче, тому що обидва виміри
rect2 менші за виміри rect1, але rect3 ширший за
rect1:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Ми знаємо, що хочемо визначити метод, отже він буде в блоці impl Rectangle.
Ім’я методу буде can_hold, і він прийматиме незмінне запозичення
іншого Rectangle як параметр. Ми можемо зрозуміти, яким буде тип
параметра, подивившись на код, що викликає метод:
rect1.can_hold(&rect2) передає &rect2, яке є незмінним запозиченням rect2, екземпляра Rectangle.
Це має сенс, тому що нам потрібно лише читати rect2 (а не записувати, що означало б, що нам знадобиться змінне запозичення),
і ми хочемо, щоб main зберіг володіння над rect2, щоб ми могли використати його знову
після виклику методу can_hold. Значення, що повертається can_hold, буде логічним, а реалізація перевірятиме, чи ширина і висота
self більші за ширину і висоту іншого Rectangle
відповідно. Давайте додамо новий метод can_hold до блоку impl із
Лістингу 5-13, показаного в Лістингу 5-15.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
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
}
}
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));
}
Коли ми запустимо цей код із функцією main з Лістингу 5-14, ми отримаємо бажаний
вивід. Методи можуть приймати кілька параметрів, які ми додаємо до
сигнатури після параметра self, і ці параметри працюють так само, як
параметри у функціях.
Асоційовані функції
Усі функції, визначені в блоці impl, називаються асоційованими функціями
тому що вони пов’язані з типом, названим після impl. Ми можемо визначати
асоційовані функції, які не мають self як свій перший параметр (і отже
не є методами), тому що їм не потрібен екземпляр типу, з яким працювати.
Ми вже використали одну таку функцію: функцію String::from, яка
визначена для типу String.
Асоційовані функції, які не є методами, часто використовуються як конструктори, що
повертатимуть новий екземпляр структури. Їх часто називають new, але
new — не спеціальне ім’я і не вбудоване в мову. Наприклад, ми
могли б вирішити надати асоційовану функцію з назвою square, яка мала б
один параметр розміру та використовувала б його і як ширину, і як висоту, таким чином полегшуючи
створення квадратної Rectangle замість того, щоб указувати одне й те саме
значення двічі:
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Ключові слова Self у типі, що повертається, та в тілі функції
є псевдонімами для типу, який стоїть після ключового слова impl, яким у цьому випадку
є Rectangle.
Щоб викликати цю асоційовану функцію, ми використовуємо синтаксис :: з назвою структури;
let sq = Rectangle::square(3); — це приклад. Ця функція має простір імен структури: Синтаксис :: використовується як для асоційованих функцій, так і для
просторів імен, створених модулями. Ми обговоримо модулі в Розділі
7.
Кілька блоків impl
Кожна структура може мати кілька блоків impl. Наприклад, Лістинг
5-15 еквівалентний коду, показаному в Лістингу 5-16, який має кожен метод у
власному блоці impl.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
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));
}
це допустимий синтаксис. Ми побачимо випадок, у якому кілька блоків impl є
корисними, у Розділі 10, де ми обговорюємо узагальнені типи та трейти.
Підсумок
Структури дають змогу створювати власні типи, які мають значення для вашої предметної області. Використовуючи
структури, ви можете тримати пов’язані частини даних разом
і називати кожну частину, щоб зробити ваш код зрозумілим. У блоках impl ви можете визначати
функції, пов’язані з вашим типом, а методи — це різновид
асоційованої функції, який дає змогу вам визначати поведінку, яку мають екземпляри ваших
структур.
Але структури — не єдиний спосіб створювати власні типи: Давайте звернемося до можливості перелічень (enums) у Rust, щоб додати ще один інструмент до вашого набору інструментів.