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