Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Методы

Методы похожи на функции: они объявляются ключевым словом fn и именем, у них могут быть параметры и возвращаемое значение, и в них есть код, который выполняется, когда метод вызывают. Но, в отличие от функций, методы определяются в контексте структуры (или перечисления, или трейт-объекта — о них мы поговорим в главе 6 и главе 18 соответственно), а их первый параметр — всегда self: это экземпляр структуры, у которого вызван метод.

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

Превратим функцию area, которая принимает экземпляр Rectangle параметром, в метод area, определённый для структуры Rectangle, как в листинге 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()
    );
}
Листинг 5-13: Определяем метод area для структуры Rectangle

Чтобы определить функцию в контексте Rectangle, открываем для Rectangle блок impl (от implementation — реализация). Всё, что внутри этого блока, будет связано с типом 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 мы выбрали по той же причине, по которой в версии с функцией писали &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. Без скобок — что поле width.

Часто (хотя и не всегда) метод с тем же именем, что и поле, просто возвращает значение этого поля и больше ничего не делает. Такие методы называются геттерами (getters), и 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);
}

Первый вариант выглядит куда чище. Автоматическое взятие ссылки работает потому, что у методов есть явный получатель (receiver) — тип self. Зная получатель и имя метода, Rust может однозначно определить, читает ли метод данные (&self), изменяет их (&mut self) или поглощает (self). То, что Rust неявно заимствует получателя метода, во многом и делает владение удобным на практике.

Методы с несколькими параметрами

Потренируемся и напишем для структуры Rectangle второй метод. Он будет принимать другой экземпляр Rectangle и возвращать true, если этот второй прямоугольник целиком помещается внутри self (первого прямоугольника), а иначе — 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));
}
Листинг 5-14: Используем ещё не написанный метод can_hold

Ожидаемый вывод будет таким: оба размера rect2 меньше размеров rect1, а вот rect3 шире, чем rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Раз мы определяем метод, он будет в блоке impl Rectangle. Назовём его can_hold; параметром он примет неизменяемое заимствование другого Rectangle. Каким должен быть тип параметра, видно из вызывающего кода: rect1.can_hold(&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));
}
Листинг 5-15: Реализуем для Rectangle метод can_hold, который принимает параметром другой экземпляр Rectangle

Если запустить этот код с функцией main из листинга 5-14, получим нужный вывод. Методы могут принимать несколько параметров — они добавляются в сигнатуру после self и работают точно так же, как параметры функций.

Ассоциированные функции

Все функции, определённые в блоке impl, называются ассоциированными функциями (associated functions), потому что они связаны с типом, указанным после 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);
}

Ключевое слово 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));
}
Листинг 5-16: Листинг 5-15, переписанный с несколькими блоками impl

Здесь разносить методы по разным блокам impl незачем, но синтаксис это допускает. Случай, когда несколько блоков impl действительно полезны, мы увидим в главе 10, где пойдёт речь об обобщённых типах и трейтах.

Итоги

Структуры позволяют создавать собственные типы, которые имеют смысл в вашей предметной области. Со структурами связанные между собой данные остаются вместе, а у каждой части есть имя, и код становится понятнее. В блоках impl можно определять функции, связанные с вашим типом, а методы — это разновидность ассоциированных функций, которая задаёт поведение экземпляров ваших структур.

Но структуры — не единственный способ создавать собственные типы. Перейдём к перечислениям Rust и добавим в ваш арсенал ещё один инструмент.