Синтаксис метода
Методы похожи на функции: мы объявляем их с помощью ключевого слова fn
и имени; они могут иметь параметры и возвращаемое значение; они содержат код, запускающийся при вызове метода. В отличие от функций, методы определяются в контексте структуры (или перечисления или трейт-объекта, которые мы рассмотрим в Главе 6 и Главе 17 соответственно), а их первым параметром всегда является 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!( "Площадь прямоугольника равна {} квадратных пикселей.", 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
здесь по той же причине, по которой использовали &Rectangle
в версии с функцией: мы не хотим брать структуру во владение, мы просто хотим прочитать данные в структуре, а не писать в неё. Если бы мы хотели изменить экземпляр, на котором мы вызывали метод, то мы бы использовали &mut 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!("Длина прямоугольника ненулевая и равна {}", 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
. С таким методом мы могли бы написать программу как в Листинге 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!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}
Ожидаемый результат будет выглядеть так, как ниже: 1) оба измерения экземпляра rect2
меньше, чем измерения экземпляра rect1
, и 2) 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
. В Листинге 5-15 приведено определение нового метода can_hold
, добавленного в блок impl
из Листинга 5-13.
#[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!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2)); println!("Вместится ли rect3 в rect1? {}", 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); }
Ключевые слова 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!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2)); println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3)); }
В данном случае, нет причин разбивать методы на несколько блоков impl
, однако это всё-таки реализуемо. Мы увидим случай, когда несколько impl
могут оказаться полезными, в Главе 10, рассматривающей обобщённые типы и трейты.
Подведём итоги
Структуры позволяют вам создавать собственные типы, которые имеют смысл в вашей предметной области. Используя структуры, вы можете хранить вместе и именовать связанные друг с другом фрагменты данных, чтобы делать ваш код чище. В блоках impl
вы можете определять 1) функции, ассоциированные с вашим типом, и 2) методы — своего рода ассоциированные функции, поведение которых зависит в том числе от экземпляров, на которых вы их вызываете.
Но структуры — не единственный способ создавать собственные типы. Давайте посмотрим на перечисления — ещё один инструмент в вашем арсенале.