Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 19-15: Реализация типажа Add для структуры Millimeters , чтобы складывать Millimeters и Meters Чтобы сложить Millimeters и Meters , мы указываем impl Add , чтобы указать значение параметра типа RHS (Meters) вместо использования значения по умолчанию Self (Millimeters). Параметры типа по умолчанию используются в двух основных случаях: Чтобы расширить тип без внесения изменений ломающих существующий код Чтобы позволить пользовательское поведение в специальных случаях, которые не нужны большинству пользователей Типаж Add из стандартной библиотеки является примером второй цели: обычно вы складываете два одинаковых типа, но типаж Add позволяет сделать больше. Использование параметра типа по умолчанию в объявлении типажа Add означает, что не нужно указывать дополнительный параметр большую часть времени. Другими словами, большая часть кода реализации не нужна, что делает использование типажа проще. use std::ops::Add; struct Millimeters ( u32 ); struct Meters ( u32 ); impl Add Millimeters { type Output = Millimeters; fn add ( self , other: Meters) -> Millimeters { Millimeters( self 0 + (other. 0 * 1000 )) } } Первая цель похожа на вторую, но используется наоборот: если вы хотите добавить параметр типа к существующему типажу, можно дать ему значение по умолчанию, чтобы разрешить расширение функциональности типажа без нарушения кода существующей реализации. Полностью квалифицированный синтаксис для устранения неоднозначности: вызов методов с одинаковым именем В Rust ничего не мешает типажу иметь метод с одинаковым именем, таким же как метод другого типажа и Rust не мешает реализовывать оба таких типажа у одного типа. Также возможно реализовать метод с таким же именем непосредственно у типа, такой как и методы у типажей. При вызове методов с одинаковыми именами в Rust нужно указать, какой из трёх возможных вы хотите использовать. Рассмотрим код в листинге 19-16, где мы определили два типажа: Pilot и Wizard , у обоих есть метод fly . Затем мы реализуем оба типажа у типа Human в котором уже реализован метод с именем fly . Каждый метод fly делает что-то своё. Файл: src/main.rs trait Pilot { fn fly (& self ); } trait Wizard { fn fly (& self ); } struct Human ; impl Pilot for Human { fn fly (& self ) { println! ( "This is your captain speaking." ); } } impl Wizard for Human { fn fly (& self ) { println! ( "Up!" ); } } impl Human { fn fly (& self ) { println! ( "*waving arms furiously*" ); } } Листинг 19-16: Два типажа определены с методом fly и реализованы у типа Human , а также метод fly реализован непосредственно у Human Когда мы вызываем fly у экземпляра Human , то компилятор по умолчанию вызывает метод, который непосредственно реализован для типа, как показано в листинге 19-17. Файл: src/main.rs Листинг 19-17: Вызов fly у экземпляра Human Запуск этого кода напечатает *waving arms furiously* , показывая, что Rust называется метод fly реализованный непосредственно у Human Чтобы вызвать методы fly у типажа Pilot или типажа Wizard нужно использовать более явный синтаксис, указывая какой метод fly мы имеем в виду. Листинг 19-18 демонстрирует такой синтаксис. Файл: src/main.rs Листинг 19-18: Указание какой метода fly мы хотим вызвать Указание имени типажа перед именем метода проясняет компилятору Rust, какую именно реализацию fly мы хотим вызвать. Мы могли бы также написать Human::fly(&person) , что эквивалентно используемому нами person.fly() в листинге 19-18, но это писание немного длиннее, когда нужна неоднозначность. Выполнение этого кода выводит следующее: Поскольку метод fly принимает параметр self , если у нас было два типа оба реализующих один типаж, то Rust может понять, какую реализацию типажа fn main () { let person = Human; person.fly(); } fn main () { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); } $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.46s Running `target/debug/traits-example` This is your captain speaking. Up! *waving arms furiously* использовать в зависимости от типа self Однако ассоциированные функции являющиеся частью типажей не имеют self параметра. Когда два типа в одной области видимости реализуют такой типаж, Rust не может выяснить, какой тип вы имеете в виду если вы не используете полностью квалифицированный синтаксис (fully qualified). Например, типаж Animal в листинге 19-19 имеет: ассоциированную функцию baby_name , реализацию типажа Animal для структуры Dog и ассоциированную функцию baby_name , объявленную напрямую у структуры Dog Файл: src/main.rs Листинг 19-19: Типаж с ассоциированной функцией и тип с ассоциированной функцией с тем же именем, которая тоже реализует типаж Этот код для приюта для животных, который хочет назвать всех щенков именем Spot, что реализовано в ассоциированной функции baby_name , которая определена для Dog . Тип Dog также реализует типаж Animal , который описывает характеристики, которые есть у всех животных. Маленьких собак называют щенками, и это выражается в реализации Animal у Dog в функции baby_name ассоциированной с типажом Animal В main мы вызываем функцию Dog::baby_name , которая вызывает ассоциированную функцию определённую напрямую у Dog . Этот код печатает следующее: trait Animal { fn baby_name () -> String ; } struct Dog ; impl Dog { fn baby_name () -> String { String ::from( "Spot" ) } } impl Animal for Dog { fn baby_name () -> String { String ::from( "puppy" ) } } fn main () { println! ( "A baby dog is called a {}" , Dog::baby_name()); } $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.54s Running `target/debug/traits-example` A baby dog is called a Spot Этот вывод является не тем, что мы хотели получить. Мы хотим вызвать функцию baby_name , которая является частью типажа Animal реализованного у Dog , так чтобы код печатал A baby dog is called a puppy . Техника указания имени типажа использованная в листинге 19-18 здесь не помогает; если мы изменим main код как в листинге 19-20, мы получим ошибку компиляции. Файл: src/main.rs Листинг 19-20. Попытка вызвать функцию baby_name из типажа Animal , но Rust не знает какую реализацию использовать Так как Animal::baby_name является ассоциированной функцией не имеющей self параметра в сигнатуре, а не методом, то Rust не может понять, какую реализацию Animal::baby_name мы хотим вызвать. Мы получим эту ошибку компилятора: Чтобы устранить неоднозначность и сказать Rust, что мы хотим использовать реализацию Animal для Dog , нужно использовать полный синтаксис. Листинг 19-21 демонстрирует, как использовать полный синтаксис. Файл: src/main.rs Листинг 19-21: Использование полностью квалифицированного синтаксиса для указания, что мы мы хотим вызвать функцию baby_name у типажа Animal реализованную в Dog Мы указываем аннотацию типа в угловых скобках, которая указывает на то что мы хотим вызвать метод baby_name из типажа Animal реализованный в Dog , также указывая что мы хотим рассматривать тип Dog в качестве Animal для вызова этой функции. Этот код теперь напечатает то, что мы хотим: fn main () { println! ( "A baby dog is called a {}" , Animal::baby_name()); } $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0283]: type annotations needed --> src/main.rs:20:43 | 20 | println!("A baby dog is called a {}", Animal::baby_name()); | ^^^^^^^^^^^^^^^^^ cannot infer type | = note: cannot satisfy `_: Animal` For more information about this error, try `rustc --explain E0283`. error: could not compile `traits-example` due to previous error fn main () { println! ( "A baby dog is called a {}" , } В общем, полностью квалифицированный синтаксис определяется следующим образом: Для ассоциированных функций при их вызове не будет receiver (объекта приёмника), а будет только список аргументов. Вы можете использовать полностью квалифицированный синтаксис везде, где вызываете функции или методы. Тем не менее, разрешается опустить любую часть этого синтаксиса, которую Rust может понять из другой информации в программе. Необходимость использования этого наиболее подробного синтаксиса возникает только в тех случаях, когда есть несколько реализаций, которые используют одинаковое имя и Rust нуждается в помощи для определения, какой вариант реализации вы хотите вызвать. Использование супер типажей для требования функциональности одного типажа в рамках другого типажа Иногда вам может понадобиться, чтобы один типаж использовал функциональность другого типажа. В этом случае нужно полагаться на зависимый типаж, который также реализуется. Типаж на который вы полагаетесь, является супер типажом типажа, который реализуете вы. Например, мы хотим создать типаж OutlinePrint с методом outline_print , который будет печатать значение обрамлённое звёздочками. Мы хотим чтобы структура Point реализующая типаж Display вывела на печать (x, y) при вызове outline_print у экземпляра Point , который имеет значение 1 для x и значение 3 для y . Она должна напечатать следующее: В реализации outline_print мы хотим использовать функциональность типажа Display . Поэтому нам нужно указать, что типаж OutlinePrint будет работать только для типов, которые также реализуют Display и предоставляют функциональность, которая нужна в OutlinePrint . Мы можем сделать это в объявлении типажа, указав OutlinePrint: Display . Этот метод похож на добавление ограничения в типаж. В листинге 19-22 показана реализация типажа OutlinePrint $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/traits-example` A baby dog is called a puppy ********** * * * (1, 3) * * * ********** Файл: src/main.rs Листинг 19-22: Реализация типажа OutlinePrint которая требует функциональности типажа Display Поскольку мы указали, что типаж OutlinePrint требует типажа Display , мы можем использовать функцию to_string , которая автоматически реализована для любого типа реализующего Display . Если бы мы попытались использовать to_string не добавляя двоеточие и не указывая типаж Display после имени типажа, мы получили бы сообщение о том, что метод с именем to_string не был найден у типа &Self в текущей области видимости. Давайте посмотрим что происходит, если мы пытаемся реализовать типаж OutlinePrint для типа, который не реализует Display , например структура Point : Файл: src/main.rs Мы получаем сообщение о том, что требуется реализация Display , но её нет: use std::fmt; trait OutlinePrint : fmt::Display { fn outline_print (& self ) { let output = self .to_string(); let len = output.len(); println! ( "{}" , "*" .repeat(len + 4 )); println! ( "*{}*" , " " .repeat(len + 2 )); println! ( "* {} *" , output); println! ( "*{}*" , " " .repeat(len + 2 )); println! ( "{}" , "*" .repeat(len + 4 )); } } struct Point { x: i32 , y: i32 , } impl OutlinePrint for Point {} Чтобы исправить, мы реализуем Display у структуры Point и выполняем требуемое ограничение OutlinePrint , вот так: Файл: src/main.rs Тогда реализация типажа OutlinePrint для структуры Point будет скомпилирована успешно и мы можем вызвать outline_print у экземпляра Point для отображения значения обрамлённое звёздочками. Шаблон Newtype для реализация внешних типажей у внешних типов В разделе "Реализация типажа у типа" главы 10, мы упоминали "правило сироты" (orphan rule), которое гласит, что разрешается реализовать типаж у типа, если либо типаж, либо тип являются локальными для нашего крейта. Можно обойти это ограничение, используя шаблон нового типа (newtype pattern), который включает в себя создание нового типа в кортежной структуре. (Мы рассмотрели кортежные структуры в разделе "Использование структур кортежей без именованных полей для создания различных типов" главы 5.) Структура кортежа будет иметь одно поле и будет тонкой оболочкой для типа которому мы хотим реализовать типаж. Тогда тип оболочки является локальным для нашего крейта и мы можем реализовать типаж для локальной обёртки. Newtype это термин, который происходит от языка программирования Haskell. В нем нет ухудшения $ cargo run Compiling traits-example v0.1.0 (file:///projects/traits-example) error[E0277]: `Point` doesn't implement `std::fmt::Display` --> src/main.rs:20:6 | 20 | impl OutlinePrint for Point {} | ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `Point` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty- print) instead note: required by a bound in `OutlinePrint` --> src/main.rs:3:21 | 3 | trait OutlinePrint: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `OutlinePrint` For more information about this error, try `rustc --explain E0277`. error: could not compile `traits-example` due to previous error use std::fmt; impl fmt::Display for Point { fn fmt (& self , f: & mut fmt::Formatter) -> fmt:: Result { write! (f, "({}, {})" , self .x, self .y) } } производительности времени выполнения при использовании этого шаблона и тип оболочки исключается во время компиляции. В качестве примера, мы хотим реализовать типаж Display для типа Vec , где "правило сироты" (orphan rule) не позволяет нам этого делать напрямую, потому что типаж Display и тип Vec объявлены вне нашего крейта. Мы можем сделать структуру Wrapper , которая содержит экземпляр Vec ; тогда мы можем реализовать Display у структуры Wrapper и использовать значение Vec как показано в листинге 19-23. Файл: src/main.rs Листинг 19-23. Создание типа Wrapper Vec для реализации Display Реализация Display использует self.0 для доступа к внутреннему Vec , потому что Wrapper это структура кортежа, а Vec это элемент с индексом 0 в кортеже. Затем мы можем использовать функциональные возможности типа Display у Wrapper Недостатком использования этой техники является то, что Wrapper является новым типом, поэтому он не имеет методов для значения, которое он держит в себе. Мы должны были бы реализовать все методы для Vec непосредственно во Wrapper , так чтобы эти методы делегировались внутреннему self.0 , что позволило бы нам обращаться с Wrapper точно так же, как с Vec . Если бы мы хотели, чтобы новый тип имел каждый метод имеющийся у внутреннего типа, реализуя типаж Deref (обсуждается в разделе "Работа с умными указателями как с обычными ссылками с помощью Deref типажа" главы 15) у Wrapper для возвращения внутреннего типа, то это было бы решением. Если мы не хотим, чтобы тип Wrapper имел все методы внутреннего типа, например, для ограничения поведения типа Wrapper , то пришлось бы вручную реализовать только те методы, которые нам нужны. Теперь вы знаете, как используется newtype шаблон по отношению к типажам; это также полезный шаблон, даже когда типажи не используются. Давайте переключимся и посмотрим на некоторые продвинутые способы взаимодействия с системой типов Rust. use std::fmt; struct Wrapper ( Vec < String >); impl fmt::Display for Wrapper { fn fmt (& self , f: & mut fmt::Formatter) -> fmt:: Result { write! (f, "[{}]" , self 0 .join( ", " )) } } fn main () { let w = Wrapper( vec! [ String ::from( "hello" ), String ::from( "world" )]); println! ( "w = {}" , w); } Расширенные типы Система типов Rust имеет некоторые возможности, которые мы упоминали в этой книге, но ещё не обсуждали. Мы начнём с обсуждения новых типов (newtypes) в целом, по мере изучения того, почему новые типы полезны в качестве типов. Затем мы перейдём к псевдонимам, возможности похожей на новые типы (newtypes), но с немного другой семантикой. Мы также обсудим тип ! и с динамическими типами (dynamically sized type). Использование Newtype шаблона для безопасности типов и реализации абстракций Примечание. В следующем разделе предполагается, что вы прочитали предыдущий раздел "Использование шаблона Newtype для реализации внешних типажей у внешних типов" Шаблон newtype полезен для задач помимо тех, которые мы обсуждали до сих пор, включая статическое обеспечение того, чтобы значения никогда не путались и указывали единицы значения. Вы видели пример использования newtype для обозначения единиц в листинге 19-15. Вспомним, что структуры Millimeters и Meters содержат обёрнутые значения u32 в newtype. Если бы мы написали функцию с параметром типа Millimeters , мы не смогли бы скомпилировать программу, которая случайно пыталась вызвать эту функция со значением типа Meters или обычным u32 Другое использование шаблона newtype - абстрагирование от некоторых деталей реализации типа: новый тип может предоставлять открытый API, отличный от API приватного внутреннего типа, если мы напрямую использовали новый тип для ограничения доступного функционала, например. Варианты шаблона (Newtypes) также могут скрывать внутреннюю реализацию. Например, мы могли бы предоставить тип People для оборачивания типа HashMap , которой хранит идентификатор человека связанного с его именем. Код использующий People будет взаимодействовать только с предоставляемым нами открытым API, например метод добавления строки имени в коллекцию People ; этому коду не понадобилось бы знать, что мы внутри присваиваем ID код типа i32 именам. Шаблон newtype - это лёгкий способ добиться инкапсуляции, скрыть детали реализации, которые мы обсуждали в разделе "Инкапсуляция, которая скрывает детали реализации" главы 17. Создание синонимов типа с помощью псевдонимов типа Наряду с шаблоном newtype, Rust предоставляет возможность объявить псевдоним типа чтобы дать существующему типу другое имя. Для этого мы используем ключевое слово type . Например, мы можем создать псевдоним типа Kilometers для i32 следующим образом: Теперь псевдоним Kilometers является синонимом для i32 ; в отличие от типов Millimeters и Meters , которые мы создали в листинге 19-15, Kilometers не являются отдельными, новыми типами. Значения с типом Kilometers будут обрабатываться так же, как значения типа i32 : Поскольку Kilometers и i32 являются одинаковым типом, мы можем сложить значения обоих типы и мы можем передать значения Kilometers в функции, которые принимают параметры типа i32 . Однако, используя этот метод, мы не получаем преимуществ проверки типа, которые доступны в шаблоне newtype, обсуждавшемся ранее. Синонимы в основном используются для уменьшения повторяемости. Например, у нас есть тип: Запись этого длинного типа в сигнатурах функций и в виде аннотаций типов по всему коду может быть утомительной и приводить к ошибкам. Представьте, что у вас есть проект, полный кодом как в листинге 19-24. Листинг 19-24: Использование длинного типа во многих местах Псевдоним типа делает этот код более управляемым за счёт сокращения повторений. В листинге 19-25 мы представили псевдоним Thunk для "многословного" типа и теперь можем заменить все использования такого типа на более короткий псевдонимом Thunk type Kilometers = i32 ; type Kilometers = i32 ; let x: i32 = 5 ; let y: Kilometers = 5 ; println! ( "x + y = {}" , x + y); Box < dyn Fn () + Send + 'static > let f: Box < dyn Fn () + Send + 'static > = Box ::new(|| println! ( "hi" )); fn takes_long_type (f: Box < dyn Fn () + Send + 'static >) { // --snip-- } fn returns_long_type () -> Box < dyn Fn () + Send + 'static > { // --snip-- } Листинг 19-25: Представление псевдонима Thunk для уменьшения количества повторений Этот код намного легче читать и писать! Выбор значимого имени для псевдоним типа может также помочь сообщить о ваших намерениях (thunk является словом для кода, который будет вычисляться позднее, так что это подходящее название для замыкания, которое сохраняется). Псевдоним типы также обычно используются с типом Result для сокращения повторения. Рассмотрим модуль std::io в стандартной библиотеке. Операции ввода/ вывода часто возвращают тип Result для обработки ситуаций, когда операция не выполняется из-за ошибки. Эта библиотека имеет структуру std::io::Error которая представляет все возможные ошибки ввода/вывода. Многие функции в библиотеке std::io будут возвращать Result , где E - это std::io::Error , например, такие как функции в Write типаже: Тип Result<..., Error> многократно повторяется. Таким образом, std::io имеет этот тип как объявление псевдонима: Поскольку это объявление находится в модуле std::io , мы можем использовать полностью квалифицированный псевдоним std::io::Result , что является Result с типом E заполненным типом std::io::Error . Сигнатуры функций типажа Write в конечном итоге выглядят как: type Thunk = Box < dyn Fn () + Send + 'static >; let f: Thunk = Box ::new(|| println! ( "hi" )); fn takes_long_type (f: Thunk) { // --snip-- } fn returns_long_type () -> Thunk { // --snip-- } use std::fmt; use std::io::Error; pub trait Write { fn write (& mut self , buf: &[ u8 ]) -> Result < usize , Error>; fn flush (& mut self ) -> Result <(), Error>; fn write_all (& mut self , buf: &[ u8 ]) -> Result <(), Error>; fn write_fmt (& mut self , fmt: fmt::Arguments) -> Result <(), Error>; } type Result Result Псевдоним типа помогает двумя способами: он облегчает написание кода и даёт нам согласованный интерфейс для всего из std::io . Поскольку это псевдоним, то это просто ещё один тип Result , что означает, что с ним мы можем использовать любые методы, которые работают с Result , а также специальный синтаксис вроде ? оператора. Тип Never, который никогда не возвращается Rust имеет специальный тип с названием ! , который известен в теории типов как пустой тип (empty type), потому что у него нет значений. Мы предпочитаем называть его тип никогда (never type), потому что он стоит на месте возвращаемого типа, такая функция никогда не возвращает управление. Вот пример: Этот код читается как «функция bar никогда не возвращается». Функции, которые никогда не возвращаются называются расходящимися функциями (diverging functions). Нельзя создавать значения типа ! , так как bar никогда не может вернуться. Но для чего нужен тип, для которого вы никогда не сможете создать значения? Напомним код из листинга 2-5; мы воспроизвели его часть здесь в листинге 19-26. Листинг 19-26: Сопоставление match с веткой, которая заканчивается continue В то время мы опустили некоторые детали в этом коде. В главе 6 раздела "Оператор управления потоком match " мы обсуждали, что все ветви match должны возвращать одинаковый тип. Например, следующий код не работает: pub trait Write { fn write (& mut self , buf: &[ u8 ]) -> Result < usize >; fn flush (& mut self ) -> Result <()>; fn write_all (& mut self , buf: &[ u8 ]) -> Result <()>; fn write_fmt (& mut self , fmt: fmt::Arguments) -> Result <()>; } fn bar () -> ! { // --snip-- } let guess: u32 = match guess.trim().parse() { Ok (num) => num, Err (_) => continue , }; let guess = match guess.trim().parse() { Ok (_) => 5 , Err (_) => "hello" , }; Тип guess в этом коде должен быть целым числом и строкой и Rust требует, чтобы guess имел только один тип. Так что тогда возвращает код continue ? Как нам разрешили вернуть u32 из одной ветки и иметь другую ветку заканчивающуюся на continue в листинге 19-26? Как вы уже возможно догадались, continue имеет значение ! . То есть, когда Rust вычисляет тип guess , он смотрит на обе сопоставляемые ветки, первая со значением u32 и последняя со значением ! . Так как ! никогда не может иметь значение, то Rust решает что типом guess является тип u32 Формальным способом описания этого поведения является то, что выражения типа ! могу быть приведены (coerced) к любому другому типу. Нам разрешено закончить сопоставление этой match ветки с помощью continue , потому что continue не возвращает значение; вместо этого она передаёт контроль обратно в начало цикла, поэтому в случае Err мы никогда не присваиваем guess значение. Never тип полезен также с макросом panic! . Помните, функцию unwrap , которую мы вызываем для значений Option , чтобы создать значение или вызвать панику? Вот её определение: В этом коде происходит то же самое, что и в выражении match из листинга 19-26: Rust видит, что val имеет тип T и panic! имеет тип ! , поэтому общим результатом match выражения является T . Этот код работает, потому что panic! не производит значения; он завершает выполнение программы. В случае None , мы не будем возвращать значение из unwrap , поэтому этот код действительный. Последнее выражение, которое имеет тип ! это loop : Здесь цикл никогда не заканчивается, так что ! (never type) является значением выражения. Тем не менее, это не будет правдой, если мы добавим в цикл break , потому что цикл мог бы завершится, когда дело дойдёт до break impl Option ( self ) -> T { match self { Some (val) => val, None => panic! ( "called `Option::unwrap()` on a `None` value" ), } } } print! ( "forever " ); loop { print! ( "and ever " ); } Динамические типы и Sized типаж В связи с необходимостью Rust знать определённые детали, например, сколько места выделять для значения определённого типа, то существует краеугольный камень его системы типов, который может сбивать с толку. Это концепция динамических типов (dynamically sized types). Иногда она упоминается как DST или безразмерные типы (unsized types), эти типы позволяют писать код, используя значения, чей размер известен только во время выполнения. Давайте углубимся в детали динамического типа str , который мы использовали на протяжении всей книги. Все верно, не типа &str , а типа str самого по себе, который является DST. Мы не можем знать, какой длины строка до момента времени выполнения, то есть мы не можем создать переменную типа str и не можем принять аргумент типа str . Рассмотрим следующий код, который не работает: Rust должен знать, сколько памяти выделить для любого значения конкретного типа и все значения типа должны использовать одинаковый объем памяти. Если Rust позволил бы нам написать такой код, то эти два значения str должны были бы занимать одинаковое количество памяти. Но они имеют разную длину: s1 нужно 12 байтов памяти, а для s2 нужно 15. Вот почему невозможно создать переменную имеющую динамический тип. Так что же нам делать? В этом случае вы уже знаете ответ: мы делаем типы s1 и s2 в виде типа &str , а не str . Напомним, что в разделе "Строковые срезы" главы 4, мы сказали, что структура данных срез хранит начальную позицию и длину среза. Таким образом, хотя &T является единственным значением, которое хранит адрес памяти где находится тип T , тип &str является двумя значениями: адресом str и его длиной. Таким образом, мы можем знать размер значения &str во время компиляции: это двойная длина от типа usize . То есть мы всегда знаем размер &str , неважно какой длины является строка на которую она ссылается. В общем, это способ которым в Rust используются динамические типы: у них есть дополнительные метаданные в которых хранится размер динамической информации. Золотое правило динамических типов в том, что мы всегда должны ставить значения динамических типов позади некоторого указателя. Можно комбинировать str со всеми видами указателей: например, Box или Rc . На самом деле, вы видели это раньше, но с другим динамическим типом: типажом. Каждый типаж является динамическим типом к которому можно обратиться используя имя типажа. В разделе "Использование объектов-типажей, которые разрешаю использовать разные значения типов" главы 17, мы упоминали, что для использования типажей в качестве объектов-типажей мы должны поместить их за указателем, например &dyn Trait или Box ( Rc тоже будет работать). let s1: str = "Hello there!" ; let s2: str = "How's it going?" ; Для работы с DST в Rust есть особый типаж, называемый Sized для определения, известен ли размер типа во время компиляции. Этот типаж автоматически реализуется для всех типов, чей размер известен во время компиляции. Кроме того, Rust неявно добавляет ограничение Sized в каждую обобщённую функцию. То есть определение обобщённой функции написанное как: на самом деле рассматривается как если бы мы написали её в виде: По умолчанию обобщённые функции будут работать только с типами чей размер известен в время компиляции. Тем не менее, можно использовать следующий специальный синтаксис, чтобы ослабить это ограничение: Ограничение на типаж ?Sized означает « T может или не может быть Sized », и это обозначение имеет приоритет по умолчанию. Общие типы должны иметь известный размер во время компиляции. Синтаксис ?Trait с таким значением доступен только для Sized , но не для любых других типажей. Также обратите внимание, что мы поменяли тип параметра t с T на &T . Поскольку тип мог бы не быть Sized , мы должны использовать его за каким-либо указателем. В в этом случае мы выбрали ссылку. Далее мы поговорим о функциях и замыканиях! fn generic // --snip-- } fn generic >(t: T) { // --snip-- } fn generic >(t: &T) { // --snip-- } Продвинутые функции и замыкания Наконец, мы рассмотрим некоторые дополнительные возможности, связанные с функциями и замыкания, которые включают указатели на функции и возврат замыканий. Указатели функций Мы говорили о том, как передавать замыкания в функции; но вы также можете передавать обычные функции в функции! Эта техника полезна, когда вы хотите передать функцию, которую вы уже определили, а не объявлять новое замыкание. Указатель функции позволит использовать функции как аргументы к другим функциям. Функции приводятся (coerce) к типу fn (с нижним регистром f), не к путать с типажом замыкания Fn . Тип fn называется указателем функции. Синтаксис для указания того, что параметр является указателем функции, похож на замыкание как показано в листинге 19-27. Файл: src/main.rs Листинг 19-27: Использование типа fn для принятия указателя функции в качестве аргумента Этот код печатает The answer is: 12 . Мы указываем, что параметр вызова f для функции do_twice является fn , которая принимает один параметр типа i32 и возвращает тип i32 . Затем мы можем вызвать f в теле функции do_twice . В main показано как можно передать имя функции add_one в качестве первого аргумента для функции do_twice В отличие от замыканий, fn является типом, а не типажом, поэтому мы указываем fn как параметр типа напрямую, а не объявляем параметр обобщённого типа с одним из типажей Fn в качестве ограничения типажа. Указатели функций реализуют все три типажа замыканий ( Fn , FnMut и FnOnce ), поэтому вы всегда можете передать указатель функции в качестве аргумента функции ожидающей замыкание. Лучше всего объявлять функции, используя обобщённый тип и fn add_one (x: i32 ) -> i32 { x + 1 } fn do_twice (f: fn ( i32 ) -> i32 , arg: i32 ) -> i32 { f(arg) + f(arg) } fn main () { let answer = do_twice(add_one, 5 ); println! ( "The answer is: {}" , answer); } одним из типажей замыкания, так что ваши функции могут принимать либо функции, либо замыкания. Пример того, где вы хотели бы принимать только тип fn , а не замыкания является взаимодействие с внешним кодом, который не имеет замыканий: функции в C могут принимать функции в качестве аргументов, но C не имеет замыканий. Для примера того, где вы могли бы использовать либо замыкание, определённое как встроенное, либо именованную функцию, давайте посмотрим на использование map Для использования функции map , чтобы превратить вектор чисел в вектор строк, мы могли бы использовать замыкание, как здесь: Или мы могли бы назвать функцию вместо замыкания в качестве аргумента при вызове map , как здесь: Обратите внимание, что мы должны использовать полный синтаксис, о котором мы говорили ранее в разделе "Расширенные типажи" , потому что доступно несколько функций с именем to_string . Здесь мы используем функцию to_string определённую в типаже ToString , который реализован в стандартной библиотеке для любого типа реализующего типаж Display У нас есть ещё один полезный шаблон, который использует детали реализации структур кортежей (tuple structs) и вариантов перечислений структур кортежей (tuple-struct enum). Эти типы используют () в качестве синтаксиса инициализатора, который выглядит как вызов функции. Инициализаторы на самом деле реализованы как функции, возвращающие экземпляр, который построен из их аргументов. Мы можем использовать эти функции инициализаторы как указатели на функции, которые реализуют типажи замыканий, что означает мы можем указать инициализирующие функции в качестве аргументов для методов, которые принимают замыкания, например: Здесь мы создаём экземпляры Status::Value , используя каждое значение u32 в диапазоне (0..20), с которым вызывается map с помощью функции инициализатора Status::Value . Некоторые люди предпочитают этот стиль, а некоторые предпочитают let list_of_numbers = vec! [ 1 , 2 , 3 ]; let list_of_strings: Vec < String > = list_of_numbers.iter().map(|i| i.to_string()).collect(); let list_of_numbers = vec! [ 1 , 2 , 3 ]; let list_of_strings: Vec < String > = list_of_numbers.iter().map( ToString ::to_string).collect(); enum Status { Value( u32 ), Stop, } let list_of_statuses: Vec 0u32 20 ).map(Status::Value).collect(); использовать замыкания. Оба варианта компилируется в один и тот же код, поэтому используйте любой стиль, который вам понятнее. Возврат замыканий Замыкания представлены типажами, что означает невозможность напрямую вернуть замыкания. В большинстве случаев, когда вы возможно хотите вернуть типаж, вы вместо этого используете конкретный тип, который реализует типаж в качестве возвращаемого значения функции. Но вы не можете сделать этого с замыканиями, потому что у них нет конкретного типа, который можно вернуть; не разрешается использовать указатель функции fn в качестве возвращаемого типа, например. Следующий код пытается напрямую вернуть замыкание, но он не компилируется: Ошибка компилятора выглядит следующим образом: Ошибка снова ссылается на типаж Sized ! Rust не знает, сколько памяти нужно будет выделить для замыкания. Мы видели решение этой проблемы ранее. Мы можем использовать типаж-объект: fn returns_closure () -> dyn Fn ( i32 ) -> i32 { |x| x + 1 } $ cargo build Compiling functions-example v0.1.0 (file:///projects/functions-example) error[E0746]: return type cannot have an unboxed trait object --> src/lib.rs:1:25 | 1 | fn returns_closure() -> dyn Fn(i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = note: for information on `impl Trait`, see |