Синтаксис методів

Методи подібні до функцій: вони проголошуються ключовим словом 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()
    );
}

Listing 5-13: Defining an area method on the Rectangle struct

Щоб визначити функцію в контексті 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));
}

Listing 5-14: Using the as-yet-unwritten can_hold method

Очікуване виведення буде виглядати наступним чином, оскільки обидва виміри 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.

Файл: 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));
}

Listing 5-15: Implementing the can_hold method on Rectangle that takes another Rectangle instance as a parameter

Коли ми запустимо цей код з функції 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));
}

Listing 5-16: Rewriting Listing 5-15 using multiple impl blocks

Тут немає підстав розділяти ці методи у декілька блоків 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.