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