Методы
Методы похожи на функции: они объявляются ключевым словом fn и именем, у них
могут быть параметры и возвращаемое значение, и в них есть код, который
выполняется, когда метод вызывают. Но, в отличие от функций, методы
определяются в контексте структуры (или перечисления, или трейт-объекта — о
них мы поговорим в главе 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()
);
}
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:
#[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.
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_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.
#[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));
}
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));
}
implЗдесь разносить методы по разным блокам impl незачем, но синтаксис это
допускает. Случай, когда несколько блоков impl действительно полезны, мы
увидим в главе 10, где пойдёт речь об обобщённых типах и трейтах.
Итоги
Структуры позволяют создавать собственные типы, которые имеют смысл в вашей
предметной области. Со структурами связанные между собой данные остаются
вместе, а у каждой части есть имя, и код становится понятнее. В блоках impl
можно определять функции, связанные с вашим типом, а методы — это
разновидность ассоциированных функций, которая задаёт поведение экземпляров
ваших структур.
Но структуры — не единственный способ создавать собственные типы. Перейдём к перечислениям Rust и добавим в ваш арсенал ещё один инструмент.