типажейБыло бы полезно иметь возможность печатать экземпляр Rectangle во время отладки программы и видеть значения всех полей. Листинг 5-11 использует макрос println!,который мы уже использовали в предыдущих главах. Тем не менее, это не работает.structRectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Файл: src/main.rs
Листинг 5-11: Попытка вывести значения экземпляра
Rectangle
При компиляции этого кода мы получаем ошибку с сообщением:
Макрос println!
умеет выполнять множество видов форматирования, и по умолчанию фигурные скобки в println!
означают использование форматирование, известное как типаж
Display
. Его вывод предназначен для непосредственного использования конечным пользователем. Примитивные типы, изученные ранее, по умолчанию реализуют типаж
Display
, потому что есть только один способ отобразить число
1
или любой другой примитивный тип. Но для структур форматирование println!
менее очевидно, потому что есть гораздо больше способов отображения: Вы хотите запятые или нет? Вы хотите печатать фигурные скобки? Должны ли отображаться все поля? Из-за этой неоднозначности Rust не пытается угадать, что нам нужно, а структуры не имеют встроенной реализации
Display для использования в println!
с заполнителем
{}
Продолжив чтение текста ошибки, мы найдём полезное замечание:
Давайте попробуем! Вызов макроса println!
теперь будет выглядеть так println!
("rect1 is {:?}", rect1);
. Ввод спецификатора
:?
внутри фигурных скобок говорит макросу println!
, что мы хотим использовать другой формат вывода, известный как
Debug
. Типаж
Debug позволяет печатать структуру способом, удобным для разработчиков, чтобы видеть значение во время отладки кода.
Скомпилируем код с этими изменениями. Упс! Мы всё ещё получаем ошибку:
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"rect1 is {}"
, rect1);
} error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty- print) instead error[E0277]: `Rectangle` doesn't implement `Debug`
Снова компилятор даёт нам полезное замечание:
Rust реализует функциональность для печати отладочной информации, но не включает
(не выводит) её по умолчанию. Мы должны явно включить эту функциональность для нашей структуры. Чтобы это сделать, добавляем внешний атрибут
#[derive(Debug)]
сразу перед определением структуры, как показано в листинге 5-12.
Файл: src/main.rs
Листинг 5-12: добавление атрибута для вывода типажа
Debug
и печати экземпляра
Rectangle
с
отладочным форматированием
Теперь при запуске программы мы не получим ошибок и увидим следующий вывод:
Отлично! Это не самый красивый вывод, но он показывает значения всех полей экземпляра, которые определённо помогут при отладке. Когда у нас более крупные структуры, то полезно иметь более простой для чтения вывод; в таких случаях можно использовать код
{:#?}
вместо
{:?}
в строке макроса println!
. В этом примере использование стиля
{:#?}
приведёт к такому выводу:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for
Rectangle`
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"rect1 is {:?}"
, rect1);
}
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50 }
Другой способ распечатать значение в формате
Debug
— использовать макрос dbg!
,
который становится владельцем выражения (в отличие от println!
, принимающего ссылку), печатает номер файла и строки, где происходит вызов макроса dbg!
, вместе с результирующим значением этого выражения и возвращает владение на значение.
Примечание: при вызове макроса dbg!
выполняется печать в стандартный поток ошибок (
stderr
), в отличие от println!
, который использует стандартный поток вывода в консоль (
stdout
). Подробнее о stderr и stdout мы поговорим в разделе
«Запись сообщений об ошибках в стандартный вывод ошибок вместо стандартного вывода» главы 12
Вот пример, когда нас интересует значение, которое присваивается полю width
, а также значение всей структуры в rect1
:
Можем написать макрос dbg!
вокруг выражения
30 * scale
, потому что dbg!
возвращает владение значения выражения. Поле width получит то же значение, как если бы у нас не было вызова dbg!
. Мы не хотим, чтобы макрос dbg!
становился владельцем rect1
, поэтому используем ссылку на rect1
в следующем вызове. Вот как выглядит вывод этого примера:
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50,
}
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let scale =
2
; let rect1 = Rectangle { width: dbg!(
30
* scale), height:
50
,
}; dbg!(&rect1);
}
Мы можем увидеть, что первый отладочный вывод поступил из строки 10 src/main.rs, там,
где мы отлаживаем выражение
30 * scale
, и его результирующее значение равно 60
(
Debug форматирование, реализованное для целых чисел, заключается в печати только их значения). Вызов dbg!
в строке 14 src/main.rs выводит значение
&rect1
, которое является структурой
Rectangle
. В этом выводе используется красивое форматирование
Debug типа
Rectangle
. Макрос dbg!
может быть очень полезен, когда вы пытаетесь понять, что делает ваш код!
В дополнение к
Debug
, Rust предоставил нам ряд типажей, которые мы можем использовать с атрибутом derive для добавления полезного поведения к нашим пользовательским типам. Эти типажи и их поведение перечислены в приложении C
. Мы расскажем, как реализовать эти трейты с пользовательским поведением, а также как создать свои собственные трейты в главе 10. Кроме того, есть много других атрибутов помимо derive
; для получения дополнительной информации смотрите раздел
“Атрибуты” справочника Rust
Функция area является довольно специфичной: она считает только площадь прямоугольников. Было бы полезно привязать данное поведение как можно ближе к структуре
Rectangle
, чтобы он не мог работать с любым другим типом. Давайте рассмотрим, как можно улучшить наш код, превращая функцию area в метод area
,
определённый для типа
Rectangle
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50,
}
Синтаксис метода
Методы похожи на функции: мы объявляем их с помощью ключевого слова fn и имени,
они могут иметь параметры и возвращаемое значение, и они содержат код,
запускающийся в случае вызова метода. В отличие от функций, методы определяются в контексте структуры (или перечисления, или типаж-объекта, которые мы рассмотрим в
Главах 6 и 17 соответственно), и их первым параметром всегда является self
,
представляющий собой экземпляр структуры, на которой вызывается этот метод.
Определение методов
Давайте изменим функцию area так, чтобы она имела экземпляр
Rectangle в качестве входного параметра и сделаем её методом area
, определённым для структуры
Rectangle
, как показано в листинге 5-13:
Файл: src/main.rs
Листинг 5-13: Определение метода
area
для структуры
Rectangle
Чтобы определить функцию в контексте
Rectangle
, мы создаём блок impl
(implementation - реализация) для
Rectangle
. Всё в impl будет связано с типом
Rectangle
. Затем мы перемещаем функцию area внутрь фигурных скобок impl и
меняем первый (и в данном случае единственный) параметр на self в сигнатуре и в теле. В main
, где мы вызвали функцию area и передали rect1
в качестве аргумента,
теперь мы можем использовать синтаксис метода для вызова метода area нашего
#[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
. Синтаксис метода идёт после экземпляра: мы добавляем точку, за которой следует имя метода, круглые скобки и любые аргументы.
В сигнатуре 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
Здесь мы определили, чтобы метод 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, в котором есть такое поведение.
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);
}
}
Вот как это работает: когда вы вызываете метод object.something()
, Rust автоматически добавляет
&
,
&mut или
*
, таким образом, чтобы object соответствовал сигнатуре метода. Другими словами, это то же самое:
Первый пример выглядит намного понятнее. Автоматический вывод ссылки работает потому, что методы имеют понятного получателя - тип self
. Учитывая получателя и имя метода, Rust может точно определить, что в данном случае делает код: читает ли метод (
&self
), делает ли изменение (
&mut self
) или поглощает
(
self
). Тот факт, что Rust делает заимствование неявным для принимающего метода, в значительной степени способствует тому, чтобы сделать владение эргономичным на практике.
Методы с несколькими параметрами
Давайте попрактикуемся в использовании методов, реализовав второй метод в структуре
Rectangle
. На этот раз мы хотим, чтобы экземпляр
Rectangle брал другой экземпляр
Rectangle и возвращал true
, если второй
Rectangle может полностью поместиться внутри self
(первый
Rectangle
); в противном случае он должен вернуть false
. То есть,
как только мы определим метод can_hold
, мы хотим иметь возможность написать программу, показанную в Листинге 5-14.
Файл: src/main.rs
Листинг 5-14: Использование ещё не написанного метода
can_hold p1.distance(&p2);
(&p1).distance(&p2); 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));
}