Rust пространство имен

Обновлено: 04.07.2024

Оригинал статьи написан живущим на вашингтонщине Иваном Сагалаевым, мужем небезызвестной Алёны C++.

Сама статья.

Работать с чистыми функциями просто: вы передаете аргументы и получаете результат, при этом нет никаких побочных эффектов. С другой стороны, если функция производит побочные эффекты, такие, как изменение собственных аргументов или же глобальных объектов, то найти причины этого трудно. Мы привыкли также, что если видим что-то вроде player.set_speed(5), то можно быть уверенным, что тут собираются изменить объект player предсказуемым способом (и, возможно, посылают некоторые сигналы куда-нибудь).

Система владения/заимствования языка Rust сложна и она создает совершенно новый класс побочных эффектов.

Простой пример
Рассмотрим этот код:

Опыт большинства программистов не подготовит к тому, что объект point вдруг становится недоступным после вызова функции is_origin()! Компилятор не позволит вам использовать его в следующей строке. Это – побочный эффект, что-то произошло с аргументом, но это совсем не то, что вы видели в других языках.

Пример посложнее
Рассмотрите парсер, который берет некоторые данные из lexer и сохраняет некоторое состояние:

Ненужная на первый взгляд consume_lexeme() является просто удобной оберткой вокруг длинной последовательности вызовов, которые я делаю в приведённом коде. lexer.next() возвращает самодостаточную лексему путем копирования данных из внутреннего буфера lexer. Но теперь мы хотим это оптимизировать, чтобы лексемы содержали только ссылки на эти данные во избежание копирования. Меняем объявление метода на следующее:

Пометка 'a явно нам говорит, что время жизни лексемы теперь связано с временем жизни ссылки на lexer, с которой мы вызываем метод .next(). Т.е. не может жить само по себе, а зависит от данных в буфере lexer. И теперь Parser::next() перестает работать:

Проще говоря, компилятор Rust говорит нам, что до тех пор, пока lexeme доступна в этом блоке кода, он не позволит нам изменить self.state – другую часть парсера. Но это вообще бессмысленно! Виновником здесь является consume_lexeme(). Хотя на самом деле нам нужен только self.lexer, мы говорим компилятору, что ссылаемся на весь парсер (обратите внимание на self). И поскольку эта ссылка может быть изменена, компилятор никому не позволит касаться любой части парсера для изменения данных, зависящих теперь от lexeme. Таким образом, мы имеем побочный эффект снова: хотя мы не меняли фактические типы в сигнатуре функции и код по-прежнему правилен и должен работать корректно, смена собственника неожиданно не позволяет ему далее компилироваться.

Даже при том, что я понял проблему в целом, мне потребовались не менее чем два дня, чтобы до меня дошло и исправление стало очевидным.

Исправляем
Изменение consume_lexeme(), позволяющее ссылаться только на lexer, а не на весь парсер, устранило проблему, но код не выглядел идиоматически из-за замены нотации с точкой на вызов обычной функции:

К счастью, Rust тоже позволяет пойти по правильному пути. Поскольку в Rust определение полей данных (struct) отличаются от определения методов (impl), я могу определить мои собственные методы для любой структуры, даже если он импортируется из другого пространства имен:

Проверка заимствований в Rust – это замечательная вещь, которая заставляет вас писать более надежный код. Но это отличается от того, к чему вы привыкли, и потребуется время для развития навыков эффективной работы.

Отзывы читателей
Juarez: У меня сложилось впечатление, что Rust добавляет излишнюю сложность внедрением «перемещение по умолчанию» для элементарных типов. Программист везде имеет дополнительное бремя boxing-ссылок. На мой взгляд, кажется, естественно думать о:
а) «копирование по умолчанию» для элементарных типов
б) «ссылка по умолчанию» для составных типов (структуры, трейты и т.д.)
в) «перемещение по умолчанию» для составных типов в асинхронных методах – от случая к случаю.
Я что-то пропустил?
Ralf: Обратите внимание, однако, что то, что вы называете «эффект» здесь на самом деле очень, очень сильно отличается от тех «эффектов», которые люди обычно имеют в виду, когда они говорят о «побочных эффектах». Понятия владения и перемещения является понятиями только время компиляции, оно не меняет того, что делает ваш код. Следовательно, это не делает рассуждения о поведении вашего кода сложнее, так как поведение не изменяется.

На самом деле побочные эффекты теперь гораздо более управляемы. Это относится, в частности, к рассуждения о неограниченных эффектах, подобным тем, которые имеет C++, где почти везде можно получить доступ ко всем видам данных под псевдонимами.

Обязанность проверки заимствования и владения – не новый побочный эффект, речь идет об ограничении существующих побочных эффектов. Если вы владеете чем-то или имеете изменяемую ссылку (которая обязательно является уникальной), вы можете быть уверенными в отсутствии неожиданных (нелокальных) побочных эффектов этого объекта, потому что никто не может иметь его псевдоним. Под этим я имею в виду, что вызов некоторой функции для некоторых данных (которыми вы владеете) никогда не будет их изменять волшебным образом. Если у вас есть разделяемая ссылка, вы можете быть уверены в отсутствии побочных эффектов, потому что никто не может изменять данные. Когда компилятор говорит вам, что данные перемещаются и вы не можете их использовать, это не новый побочный эффект. Это «простое» понимание компилятором побочных эффектов нужно для того, чтобы он мог убедиться, что всё находятся под контролем.

В C++, если вы передаёте параметр Point перед некоторой функцией, компилятор делает неполную копию, и если Point содержит указатели, то это может привести к беспорядку. Здесь объект Point является безопасным для копирования в близлежащем контексте, но вы должны явно сказать компилятору, что вы хотите:

Вы можете задаться вопросом, почему компилятор не может понять это автоматически. Было бы точно так же, как это делается для Send.
Проблемой здесь является стабильность интерфейса. Если вы пишете библиотеку, которая экспортирует тип, к которому применяется Copy, то библиотека обязана всегда сохранять этот тип Copy и в будущем. Он должен быть осознанным выбором автора библиотеки, чтобы гарантировать, что этот тип является и всегда будет являться Copy – вследствие явной аннотации.

Послесловие от переводчика: стимулом к переводу этой статьи было желание узнать, что же за «совершенно новый класс побочных эффектов» возник в Rust. Хотя в целом статья любопытна, автор находится в некотором заблуждении насчёт совершенно нового класса.

Методы похожи на функции: они объявлены с помощью ключевого слова fn и его имени. Они могут иметь параметры и возвращаемое значение, могут содержать некоторый код, который выполняется при вызове из другого места. Тем не менее, методы отличаются от функций тем, что они определены внутри контекста структуры (также перечисления или объекта-типажа, которые мы рассмотрим в главе 6 и 17, соответственно), а их первым параметром всегда является self , который представляет экземпляр структуры для которого этот метод будет вызван.

Итоги

Структуры позволяют создавать собственные типы, которые имеют смысл в вашей предметной области. Используя структуры, вы храните ассоциированные друг с другом фрагменты данных и даёте название частям данных, чтобы ваш код был более понятным. Методы позволяют определить поведение, которое имеют экземпляры ваших структур, а ассоциированные функции позволяют привязать функциональность к вашей структуре, не обращаясь к её экземпляру.

Но структуры - это не единственный способ создания пользовательских типов: давайте обратимся к перечислениям в Rust, чтобы добавить ещё один инструмент в наш арсенал.

Перед вами краткое профессиональное описание особенностей языка Rust для профессионалов.

  • краткое — информации будет гораздо меньше, чем в Книге (The Rust Programming Language)
  • профессиональное — информации будет гораздо больше, чем в Книге;
  • описание особенностей — фокусируемся на отличиях Rust от других языков;
  • языка — описывается именно язык, а не установка средств разработки, управление пакетами и прочий инструментарий;
  • для профессионалов — подразумевается, что читатель имеет существенный опыт в разработке ПО.

Чего здесь НЕ будет

  • Не будет агитации за Rust
  • Не будет легко. Для освоения потребуется неделя-другая вдумчивого чтения по часу в день, с тщательным разбором примеров, как-то так

Моя история изучения языка Rust началась со статьи Как мы ржавели. История внедрения и обучения. В ней Nurked предложил оригинальный способ прочтения Книги — читать главы надо не по порядку, а в последовательности [4, 3, 5, 6, 8, 4, 9, 7, 10, 4, 13, 17, 15, 16]. Со многими тезисами этой статьи я склонен в той или иной степени согласиться, в частности, с тем, что "первая глава этого руководства должна гореть в печи". Конечно же, вместе со второй, в которой разбирается вот этот пример:

Именно на этом примере я в свое время закрыл Книгу — неинтересно. К тому же, за println!(. ) явно скрывается некий макроязык, а к ним у меня стойкая аллергия со времен работы с Microsoft Foundation Class. Ну, в общем — нет, спасибо, не надо.

После повторной подачи от Nurked начал с главы 4 и появился интерес. Как пишут сами авторы Книги:

Владение является наиболее уникальной особенностью языка Rust. Благодаря ей в Rust осуществляется безопасная работа с памятью без необходимости использования автоматической системы сборки мусора (garbage collector).

С этого и надо было начинать. Я отношусь к той категории людей, которые несколько измучены нарзаном недовольны нюансами работы с GC в рамках highload, засим текст нашел внимательного читателя в моем лице. В процессе чтения главы 4 я обнаружил, что Книга рассчитана на совсем новичков и переполнена скучными подробностями, поэтому изначально у меня была идея просто законспектировать суть, отсекая все лишнее. Но эта идея потерпела крах, так как оказалось, что для понимания принципов работы с памятью после главы 4 надо читать главу 10.3, потом главу 15, при этом текста много, он содержит ошибки и его совершенно недостаточно. Для понимания пришлось дополнительно читать, например, это, это, это, это и вот это, но нужных мне примеров я так и не нашел.

Мне представляется, что достаточно глубокое описание особенностей языка Rust может быть дано в рамках короткой серии относительно небольших статей, без отсыла читателя к другим источникам. В процессе написания такого материала, конечно, сам получаешь хорошие знания, так что, надеюсь, затея будет выгодна как автору, так и читателям.

Читатель должен владеть на уровне middle+ двумя языками программирования (один с GC, один без него), они послужат донорами абстракций, которые в тексте не поясняются.

Из тех языков, с которыми я плотно работал, Rust ближе всего, КМК, к Go. Их роднит отсутствие "нормального ООП", отсутствие "нормальных исключений", концепция срезов (slice), наличие как объектов, так и ссылок/указателей на них, возможность возвращать несколько значений из функций, страшное слово unsafe, ну и, конечно, кросс-компиляция "из коробки". В Go пока нет обобщенных типов, но про них знают и их ждут, поэтому опытного гофера ржавчиной не испугать.

Что делать, если знаешь только один язык? План Б.

Изложение в первую очередь сфокусировано на той самой безопасной работе с памятью, которая и является основной "фишкой" Rust. Остальное часто дается "мимоходом" и подчинено главной цели.

Когда объект покидает область видимости (variable scope), его "финализируют" через вызов метода drop(). В этом деструкторе можно произвести некие завершающие действия — возвратить память в кучу, закрыть соединение и т.д. Для примера создадим экземпляр типа String с помощью конструктора фабрики String::from() :

String хранит свои данные в куче, и компилятор Rust любезно обеспечит неявный вызов s.drop() после закрывающей скобки, тем самым давая возможность вернуть использованную память.

"Книга" утверждает, что управление памятью осуществляется через "владение" с набором правил, которые компилятор проверяет во время компиляции программы. Полезно сразу иметь в виду (напомню — материал для опытных камикадзе), что есть некие:

  • Box для распределения значений в куче (памяти)
  • Rc тип счётчика ссылок, который допускает множественное владение
  • Типы Ref и RefMut , доступ к которым осуществляется через тип RefCell , который обеспечивает правила заимствования во время выполнения, вместо времени компиляции
  • Rust допускает утечки памяти, используя типы Rc и RefCel можно создавать связи, где элементы ссылаются друг на друга в цикле

Иными словами, если память выделяется в куче, а полученные объекты ссылаются друг на друга — будь готов к граблям, засадам и к поиску утечек памяти.

  • immutable_string , ого, змеиная нотация — так надо
  • Переменные, объявленные через let , изменять нельзя
  • Если надо менять используем let mut
  • Вывод на печать осуществляется причудливой конструкцией dbg! (тут может возникнуть справедливое подозрение, что это не "обычная функция")

Я думаю, Rust имеет потенциал находить отклик в сердцах многих. let как в Бейсике, <> как в Java, :: как в С++, объявление функции похоже на таковое из Go (только там func )

Ключевое правило: Каждое значение имеет одного и только одного владельца-переменную

После операции присваивания переменная типа String перестает владеть своим бывшим значением, и ее нельзя больше использовать:

Для простых типов (primitive types), однако, значение копируется, а не передается, и для них многократное присваивание выглядит обычным образом:

  • Есть trait (как бы interface) Copy , если тип его реализует, при присваивании/передаче в функцию/возврате значения происходит копирование
  • Copy реализован для простых скалярных типов, а также для неких кортежей (tuples), при условии, что эти загадочные пока tuples содержат только типы, реализующие Copy
  • Тип String не реализует Copy
  • Из неочевидного: Copy несовместим с Drop (это где drop() ). Несовместим, даже если не сам тип, а только его некоторые части реализуют Drop

При передаче переменной в функцию по значению происходит и передача владения (если тип не реализует интерфейс trait Copy ):

  • Из функции можно вернуть несколько значений (тот самый кортеж, или tuple ) означает целый беззнаковый тип, который вмещает указатель (the pointer-sized unsigned integer type)
  • return можно не писать
  • При возврате переменной "по значению" функция возвращает и владение
  • Полезное макро dbg! "съедает" параметры (то самое move), чтобы этого избежать можно использовать ссылки: dbg!(&s1, length)

Необязательно брать значение во владение, его можно "занять" (borrow):

  • Занять можно как для чтения ( & ), так и для записи ( &mut )
  • Занять для чтения (immutable borrow) можно сколько угодно раз в "области видимости переменной" (variable scope)
  • Занять для записи (mutable borrow) — только один раз
  • Нельзя занимать одновременно для чтения и записи (все это похоже на read/write locks)
  • Результат заимствования называется ссылкой (reference)

От перестановки строк из примера выше результат меняется, можно раскомментировать строки и все будет работать:

  • Здесь вроде как одновременно существуют две ссылки на чтение и одна на запись, но r1 и r2 после вывода на печать больше не используются, так что активных ссылок на момент let r3 =. — нет

Компилятор Rust гарантирует, что эта проблема искоренена полностью. Рассмотрим такой пример:

Здесь мы заводим локальную переменную и затем возвращаем ссылку на нее. Что, конечно, очень плохо, так как переменной и ее значения после возврата из функции больше нет, а ссылка на несуществующее — есть.

Компилятор откажется работать в таких условиях, в качестве причины отказа он приведет загадочную формулировку: "error[E0106]: missing lifetime specifier". Загадка lifetime specifier получит раскрытие в следующих главах.

В свое время был удивлен, что в Go таки можно вернуть указатель на локальную переменную, и за это тебе ничего не будет:

На деле хитрый компилятор в этом случае выделяет память в куче:

  • Срезы строк можно делать только по труъ-unicode-границам, иначе паника
  • Подробнее тут или here, кому что любо

Но то строки, с байтами ситуация попроще:

Теперь, собственно, про владение. Взятие среза "одалживает" всю последовательность на чтение, менять ее теперь нельзя:

Предыдущую статью восприняли лучше, чем я ожидал, так что решился на продолжение эксперимента. Это своеобразный ответ на перевод статьи Programming in D for C Programmers за авторством Дмитрия aka vintage. Как мне кажется, в области применения C Rust более уместен, чем замена Go, как предполагалось в прошлой статье. Тем интереснее будет сравнить. Опять таки, код на С приводить не буду, тем более что аналог на D всё равно смотрится лаконичнее.

Получаем размер типа в байтах

Напомню, что в С (и в С++) для этой цели существует специальный оператор sizeof , который может применяться как к типам, так и к переменным. В D же размер доступен через свойство (которое тоже можно применять и к переменным):

В Rust используется функция, которая обращается к внутренностям компилятора (соответствующему intrinsic):

При этом, по причине отсутствия перегрузки функций, для получения размера переменных используется другая функция — size_of_val . Возможно, такое разделение несколько менее удобно, зато не приходится вводить специальные ключевые слова — используются обычные языковые механизмы:

Забавный нюанс: в Rust пустые структуры (такие как Foo из примера) занимают 0 байт, соответственно массив любого размера таких структур тоже будет занимать 0 байт.
[Поиграться с кодом]

Получаем максимальное и минимальное значение типа

В D, опять-таки, используются свойства типов:

В Rust используются С-подобные константы:

Таблица соответствия типов

Сравнение не совсем правильное, так как в С используются платформозависимые типы, а в D наоборот — фиксированного размера. Для Rust подбирал именно аналоги фиксированного размера.

Особые значения чисел с плавающей точкой

Как видим, в Rust снова используются константы, которые, кстати, принято писать в верхнем регистре.

Остаток от деления вещественных чисел

Тут никаких откровений — в Rust, как и в D, имеется оператор %.

Обработка NaN значений

И в D, и в Rust сравнение с NaN даст в результате false .

Асерты — полезный механизм выявления ошибок

Оба языка предоставляют асерты "из коробки", но в D они являются специальной языковой конструкцией:

A в Rust — просто макросами:

Впрочем, есть и интересное отличие: в D асерты в релизной сборке отключаются, кроме специального случая assert(0) , который используется для обозначения недостижимого при нормальном выполнении кода.

В Rust они остаются и в релизе, впрочем, аналогичное поведение можно получить при помощи макроса debug_assert! . Для более явного обозначения недостижимого когда используется отдельный макрос unreachable! .

Итерирование по массиву (коллекции)

Особой разницы нет, хотя цикл for в Rust и не похож на своего родственника из С.

Инициализация элементов массива

В D мы можем инициализировать массив одним значением, как показано выше. Стоит заметить, что после создания массив сначала будет инициализирован значением по умолчанию того типа, который в нём содержится.

В Rust присутствует специальный синтаксис для этого случая.

Создание массивов переменной длины

D имеет встроенную поддержку массивов переменной длины:

Rust, следуя своей "философии явности", требует задать значение, которым будут инициализированы новые элементы при вызове метода resize . Поэтому правильнее пример будет записать следующим образом:

Обратите внимание, что нам не приходится указывать тип элементов содержащихся в векторе — они будут выведены автоматически.

Соединение строк

В D есть специальные перегружаемые операторы

=, предназначенные для соединения списков:

Официальная документация аргументирует наличие отдельных операторов тем, что перегрузка оператора + может приводить к неожиданностям.

В Rust, с одной стороны, эти проблемы невозможны из-за необходимости явного приведения типов. С другой стороны, оператор += для строк всё-таки не реализован.

Форматированный вывод

Как видим, языки в этом плане не особо различаются. Разве что в Rust форматирование не похоже на "привычное" из С.

Обращение к функциям до объявления

Оба языка используют модули, поэтому порядок определения не имеет значения и предварительные объявления не нужны.

Функции без аргументов

Сравнение несколько теряет смысл в отрыве от С, так как оба языка не требуют указывать void для обозначения отсутствия аргументов.

Выход из нескольких блоков кода

Синтаксис break/continue с меткой практически идентичен.

Пространство имён структур

Опять же, в обоих языках нет отдельного пространства имён для структур.

Ветвление по строковым значениям (например, обработка аргументов командной строки)

В данном случае особой разницы не видно, но в Rust конструкция match — это полноценное сравнение с образцом, что позволяет делать более хитрые вещи:

Выравнивание полей структур

В D есть специальный синтаксис, с помощью которого вы можете детально настроить выравнивание отдельных полей:

В Rust можно только полностью отключить выравнивание для отдельных структур:

Анонимные структуры и объединения

D поддерживает анонимные структуры, что позволяет сохранять плоский внешний интерфейс для вложенных сущностей:

В Rust нет анонимных структур или объединений, поэтому аналогичный код будет выглядеть вот так:

Более того, Rust не позволит случайно обратиться не к тому полю объединения, которые было инициализировано. Поэтому и обращаться к ним придётся иначе:

Таким образом, объединения нельзя использовать как (полу)легальное преобразование типов, зато исключаются потенциальные ошибки.

Определение структур и переменных

Оба языка требуют раздельного объявления типа и переменной, то есть, как на С, записать не получится:

Получение смещения поля структуры

В D у полей есть специальное свойство offsetof :

На данный момент Rust не поддерживает такую возможность, так что при необходимости вам придётся вручную вычислять смещения, манипулируя указателями на члены структуры. Впрочем, offsetof является зарезервированным ключевым словом, а значит со временем такая функциональность должна появиться.

Инициализация объединений

D требует явного указания на то, какому полю объединения присваивается значение:

Rust поступает аналогично, кроме того, как уже говорилось, он не позволит обратиться не к тому полю объединения, которое было инициализировано.

Инициализация структур

В D структуры можно инициализировать как по порядку, так и с указанием имён полей:

В Rust указание имён обязательно:

Инициализация массивов

В D существует много способов инициализации массива, в том числе с указанием индексов инициализируемых элементов:

В Rust возможно либо перечислить все значения, которыми мы хотим инициализировать массив, либо указать одно значение для всех элементов массива:

Экранирование спецсимволов в строках

Оба языка, наряду с экранированием отдельных символов, поддерживают так называемые "сырые строки":

ASCII против многобайтных кодировок

В D поддерживается несколько видов строк, которые хранят символы разного типа:

В Rust существует только один тип строк, которые представляют последовательность UTF-8 байт:

Константин aka kstep опубликовал на хабре серию переводов про строковые типы в Rust, так что если вас интересуют подробности, то рекомендую ознакомиться с ними. Ну или с официальной документацией (перевод).

Отображение перечисления на массив

Аналог на Rust с применением макроса collect! будет выглядеть следующим образом:

Создание новых типов

D позволяет создавать новые типы из имеющихся (strong typedef):

В том числе, с заданием дефолтного значения:

В Rust это делается через использование структуры-кортежа (tuple struct, перевод):

Создать значение без инициализации Rust и так не позволит, а для создания значения по умолчанию правильным будет реализовать трейт Default:

Сравнение структур

Сравнение строк

В обоих языках строки можно сравнивать на равенство и больше/меньше.

Сортировка массивов

D использует обобщённые реализации алгоритмов:

В Rust используется несколько другой подход: сортировка, как и некоторые другие алгоритмы, реализована для "срезов" (slice), а те контейнеры, для которых это имеет смысл, умеют к ним приводиться.

[Запустить]
Из мелких отличий: сравнение должно возвращать не bool , а Ordering (больше/меньше/равно).

Данное сравнение заставило задуматься, почему в Rust сделано не так как в D или С++. Навскидку не вижу преимуществ и недостатков обоих подходов, так что спишем просто на особенности языка.

Строковые литералы

Оба языка поддерживают многострочные строковые константы.

Обход структур данных

Несмотря на название, в этом пункте, по моему, демонстрируется только возможность вложенных функций обращаться к переменным объявленным в внешних, так что я взял на себя смелость переписать код:

В Rust можно объявлять вложенные функции, но захватывать переменные они не могут, для этого используются замыкания:

Динамические замыкания

В Rust тоже имеются лябмды/делегаты/замыкания. Пример был выше по тексту, ну а если вам интересны подробности, то загляните в документацию (перевод).

Переменное число аргументов

В D есть специальная конструкция ". " позволяющая принять несколько параметров в качестве одного типизированного массива:

Rust не имеет прямой поддержки переменного количества аргументов, вместо этого предлагается использовать срезы или итераторы:

Заключение

Вот и всё. Конечно, сравнение двух языков, отталкивающееся от особенностей третьего, получается довольно специфическим, но определённые выводы сделать можно. Предлагаю вам сделать их самостоятельно.

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

Давайте изменим функцию area так, чтобы она имела экземпляр Rectangle в качестве входного параметра и сделаем её методом area , определённым для структуры Rectangle , как показано в листинге 5-13:

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

Для определения функции в контексте типа Rectangle , мы начинаем блок impl (implementation - реализация). Затем переносим функцию area внутрь фигурных скобок impl и меняем первый (в данном случае единственный) параметр в сигнатуре на self , и далее везде в теле метода. В main , там где мы вызывали функцию area и передавали ей переменную rect1 в качестве аргумента, теперь можно использовать синтаксис метода для вызова метода area на экземпляре типа Rectangle . Синтаксис метода идёт после экземпляра: мы добавляем точечную нотацию за которой следует название метода, круглые скобки и любые аргументы.

В сигнатуре area , используется &self вместо rectangle: &Rectangle потому что Rust знает, что тип self является типом Rectangle , так как данный метод находится внутри impl Rectangle контекста. Заметьте, всё ещё нужно использовать & перед self , как мы делали с &Rectangle . Методы могут принимать во владение self , заимствовать неизменяемый self , как мы сделали здесь, или заимствовать изменяемый self , а также любые другие параметры.

Мы выбрали &self здесь по той же причине, по которой использовали &Rectangle в версии кода с функцией: мы не хотим брать структуру во владение, мы просто хотим прочитать данные в структуре, а не писать в неё. Если бы мы хотели изменить экземпляр, на котором мы вызывали метод силами самого метода, то мы бы использовали &mut self в качестве первого параметра. Наличие метода, который берёт экземпляр во владение, используя только self в качестве первого параметра, является редким; эта техника обычно используется, когда метод превращает self во что-то ещё, и вы хотите запретить вызывающей стороне использовать исходный экземпляр после превращения.

Основным преимуществом использования методов вместо функций, в дополнение к использованию синтаксиса метода, где не нужно повторять тип self в каждой сигнатуре метода, является организация кода. Мы собрали все, что мы можем сделать с экземпляром типа в одном блоке impl , не заставляя будущих пользователей нашего кода искать дополнительный реализованный функционал для Rectangle в разных местах библиотеки.

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

В языках C и C++, используются два различных оператора для вызова методов: используется . , если вызывается метод непосредственно у экземпляра структуры и используется -> , если вызывается метод у ссылки на объект. Другими словами, если object является ссылкой, то вызовы метода object->something() и (*object).something() являются аналогичными.

Rust не имеет эквивалента оператора -> , наоборот, в Rust есть функциональность называемая автоматическое обращение по ссылке и разыменование (automatic referencing and dereferencing). Вызов методов является одним из немногих мест в Rust, в котором есть такое поведение.

Вот как это работает: когда вы вызываете метод object.something() , Rust автоматически добавляет & , &mut или * , таким образом, чтобы object соответствовал сигнатуре метода. Другими словами, следующий код является одинаковым:

Первый пример выглядит намного понятнее. Автоматический вывод ссылки работает потому, что методы имеют понятного получателя - тип self . Учитывая получателя и имя метода, Rust может точно определить, что в данном случае делает код: читает ли метод ( &self ), делает ли изменение ( &mut self ) или поглощает ( self ). Тот факт, что Rust делает заимствование неявным для принимающего метода, в значительной степени способствует тому, чтобы сделать владение эргономичным на практике.

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

Давайте попрактикуемся в использовании методов, реализовав второй метод у структуры Rectangle . На этот раз мы хотим, чтобы экземпляр Rectangle использовал другой экземпляр типа Rectangle и возвращал true , если второй Rectangle может полностью разместиться внутри площади экземпляра self ; в противном случае он должен возвращать false . То есть мы хотим иметь возможность написать программу, показанную в листинге 5-14, в которой определили метод can_hold .

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

И ожидаемый результат будет выглядеть следующим образом, т.к. оба размера в экземпляре 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.

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

Когда мы запустим код с функцией main листинга 5-14, мы получим желаемый вывод. Методы могут принимать несколько параметров, которые мы добавляем в сигнатуру после первого параметра self , и эти параметры работают так же, как параметры в функциях.

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

Ещё одной полезной особенностью блоков impl является то, что мы можем определить функции внутри блоков impl , которые не принимают self в качестве параметра. Они называются ассоциированными функциями, потому что они всё ещё связаны со структурой в отличии от простых функций. Так же они всё ещё функции, а не методы, потому что у них нет экземпляра структуры над которой они могут работать. Вы уже использовали ассоциированную функцию String::from .

Ассоциированные функции часто используются в качестве конструкторов - функций, которые будут возвращать новый экземпляр структуры. Например, мы могли бы предоставить ассоциированную функцию, которая будет иметь один параметр измерения и использовать его как ширину и высоту, тем самым облегчая создание квадратного прямоугольника Rectangle , вместо указания одно и того же значения дважды:

Чтобы вызвать эту ассоциированную функцию, используется синтаксис :: с именем структуры; пример let sq = Rectangle::square(3); . Эта функция относится к структуре: синтаксис :: используется как для ассоциированных функций, так и для пространства имён, созданных модулями. Мы обсудим модули в Главе 7.

Несколько блоков impl

Для каждой структуры разрешено иметь множество impl блоков. Например, листинг 5-15 является эквивалентным коду из листинга 5-16, который описывает метод в своём отдельном блоке impl .

Листинг 5-16: Переписанный листинг 5-15 с использованием нескольких блоков impl

В данном случае нет причин разделять эти методы на несколько блоков impl , но это тоже является правильным синтаксисом. Мы увидим случай, в котором полезно иметь несколько impl блоков в Главе 10, где мы будем обсуждать обобщённые типы и типажи.

Читайте также: