Синтаксис методів
Методи подібні до функцій: вони проголошуються ключовим словом fn
і іменем, можуть мати параметри та повертати значення, і містять код, що виконується, коли їх викликають з іншого місця. Однак методи відрізняються від функцій тим, що визначені в контексті структури (чи енума, чи трейт-об'єкта, про що піде мова в Розділах 6 та 17, відповідно), і їхнім першим параметром завжди є self
, що представляє екземпляр структури, з якої викликається метод.
Визначення методів
Let’s change the area
function that has a Rectangle
instance as a parameter and instead make an area
method defined on the Rectangle
struct, as shown in Listing 5-13.
Файл: 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
(від implementation, "реалізація") для 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
:
Файл: src/main.rs
#[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 має особливість, що зветься автоматичне посилання і розіменування (automatic referencing and dereferencing). Виклик методів - це одне з небагатьох місць у 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.
Файл: 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));
}
And the expected output would look like the following, because both dimensions of rect2
are smaller than the dimensions of rect1
but rect3
is wider than 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.
Файл: src/main.rs
#[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
, не вказуючи одне й те саме значення двічі:
Файл: 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); }
The Self
keywords in the return type and in the body of the function are aliases for the type that appears after the impl
keyword, which in this case is 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
, але це коректний синтаксис. Ми побачимо випадок, де кілька блоків impl
можуть бути корисні, у Розділі 10, де ми поговоримо про узагальнені типи і трейти.
Підсумок
Структури дозволяють вам створювати власні типи, що мають значення для предметної області програми. Використовуючи структури, ми можемо зберігати пов’язані між собою фрагменти даних разом і давати ім'я кожному фрагменту, щоб зробити наш код зрозумілим. У блоках impl
ви можете визначити функції, асоційовані з вашим типом, а методи - це різновид асоційованих функцій, що дозволяють визначити поведінку, яку мають екземпляри ваших структур.
But structs aren’t the only way you can create custom types: let’s turn to Rust’s enum feature to add another tool to your toolbox.