Главная страница
Навигация по странице:

  • Листинг 5-11: Попытка вывести значения экземпляра

  • Листинг 5-12: добавление атрибута для вывода типажа Debug и печати экземпляра Rectangle с отладочным форматированием

  • Синтаксис метода

  • Определение методов

  • Листинг 5-13: Определение метода area для структуры

  • Где используется оператор ->

  • Методы с несколькими параметрами

  • Листинг 5-14: Использование ещё не написанного метода

  • Листинг 5-15: Реализация метода can_hold для Rectangle, принимающего другой экземпляр Rectangle в качестве параметра

  • Ассоциированные функции

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница11 из 62
    1   ...   7   8   9   10   11   12   13   14   ...   62
    типажей
    Было бы полезно иметь возможность печатать экземпляр
    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.
    1   ...   7   8   9   10   11   12   13   14   ...   62


    написать администратору сайта