Язык программирования Rust
Скачать 7.02 Mb.
|
типажей Было бы полезно иметь возможность печатать экземпляр Rectangle во время отладки программы и видеть значения всех полей. Листинг 5-11 использует макрос println! , который мы уже использовали в предыдущих главах. Тем не менее, это не работает. struct Rectangle { 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)); } И ожидаемый результат будет выглядеть следующим образом, т.к. оба размера в экземпляре rect2 меньше, чем размеры в экземпляре rect1 , а rect3 шире, чем rect1 : Мы знаем, что хотим определить метод, поэтому он будет находится в 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 Листинг 5-15: Реализация метода can_hold для Rectangle , принимающего другой экземпляр Rectangle в качестве параметра Когда мы запустим код с функцией main листинга 5-14, мы получим желаемый вывод. Методы могут принимать несколько параметров, которые мы добавляем в сигнатуру после первого параметра self , и эти параметры работают так же, как параметры в функциях. Ассоциированные функции Все функции, определённые в блоке impl , называются ассоциированными функциями, потому что они ассоциированы с типом, указанным после ключевого слова impl . Мы можем определить ассоциированные функции, которые не имеют self в качестве первого параметра (и, следовательно, не являются методами), потому что им не нужен Can rect1 hold rect2? true Can rect1 hold rect3? false 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 } } экземпляр типа для работы. Мы уже использовали одну подобную функцию: функцию String::from , определённую для типа String Ассоциированные функции, не являющиеся методами, часто используются для конструкторов, возвращающих новый экземпляр структуры. Их часто называют new , но new не является специальным именем и не встроена в язык. Например, мы можем предоставить ассоциированную функцию с именем square , которая будет иметь один параметр размера и использовать его как ширину и высоту, что упростит создание квадратного Rectangle , вместо того, чтобы указывать одно и то же значение дважды: Файл: src/main.rs Ключевые слова Self в возвращаемом типе и в теле функции являются псевдонимами для типа, указанного после ключевого слова impl , которым в данном случае является Rectangle Чтобы вызвать эту ассоциированную функцию, мы используем синтаксис :: с именем структуры; например, let sq = Rectangle::square(3); . Эта функция входит в пространство имён структуры: синтаксис :: используется как для ассоциированных функций, так и для пространств имён, созданных модулями. Мы обсудим модули в Главе 7. |