Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 15-2: Первая попытка определить перечисление в качестве структуры данных cons list, состоящей из i32 значений. Примечание: В данном примере мы реализуем cons list, который содержит только значения i32 . Мы могли бы реализовать его с помощью generics, о которых мы говорили в главе 10, чтобы определить тип cons list, который мог бы хранить значения любого типа. Использование типа List для хранения списка 1, 2, 3 будет выглядеть как код в листинге 15-3: Файл: src/main.rs Листинг 15-3: Использование перечисления List для хранения списка 1, 2, 3 enum List { Cons( i32 , List), Nil, } use crate::List::{Cons, Nil}; fn main () { let list = Cons( 1 , Cons( 2 , Cons( 3 , Nil))); } Первое значение Cons содержит 1 и другой List . Это значение List является следующим значением Cons , которое содержит 2 и другой List . Это значение List является ещё один значением Cons , которое содержит 3 и значение List , которое наконец является Nil , не рекурсивным вариантом, сигнализирующим об окончании списка. Если мы попытаемся скомпилировать код в листинге 15-3, мы получим ошибку, показанную в листинге 15-4: Листинг 15-4: Ошибка, которую мы получаем при попытке определить рекурсивное перечисление Ошибка говорит о том, что этот тип "имеет бесконечный размер". Причина в том, что мы определили List в форме, которая является рекурсивной: она непосредственно хранит другое значение своего собственного типа. В результате Rust не может определить, сколько места ему нужно для хранения значения List . Давайте разберёмся, почему мы получаем эту ошибку. Сначала мы рассмотрим, как Rust решает, сколько места ему нужно для хранения значения нерекурсивного типа. Вычисление размера нерекурсивного типа $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ recursive type has infinite size 2 | Cons(i32, List), | ---- recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box
| ++++ + error[E0391]: cycle detected when computing drop-check constraints for `List` --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ | = note: ...which immediately requires computing drop-check constraints for `List` again = note: cycle used when computing dropck types for `Canonical { max_universe: U0, variables: [], value: ParamEnvAnd { param_env: ParamEnv { caller_bounds: [], reveal: UserFacing, constness: NotConst }, value: List } }` Some errors have detailed explanations: E0072, E0391. For more information about an error, try `rustc --explain E0072`. error: could not compile `cons-list` due to 2 previous errors Вспомните перечисление Message определённое в листинге 6-2, когда обсуждали объявление enum в главе 6: Чтобы определить, сколько памяти выделять под значение Message , Rust проходит каждый из вариантов, чтобы увидеть, какой вариант требует наибольшее количество памяти. Rust видит, что для Message::Quit не требуется места, Message::Move хватает места для хранения двух значений i32 и т.д. Так как будет использоваться только один вариант, то наибольшее пространство, которое потребуется для значения Message , это пространство, которое потребуется для хранения самого большого из вариантов перечисления. Сравните это с тем, что происходит, когда Rust пытается определить, сколько места необходимо рекурсивному типу, такому как перечисление List в листинге 15-2. Компилятор смотрит на вариант Cons , который содержит значение типа i32 и значение типа List . Следовательно, Cons нужно пространство, равное размеру i32 плюс размер List . Чтобы выяснить, сколько памяти необходимо типу List , компилятор смотрит на варианты, начиная с Cons . Вариант Cons содержит значение типа i32 и значение типа List , и этот процесс продолжается бесконечно, как показано на рисунке 15-1. Cons i32 Cons i32 Cons i32 Cons i32 Cons i32 ∞ Рисунок 15-1: Бесконечный List , состоящий из нескончаемого числа вариантов Cons Использование Box Поскольку Rust не может определить, сколько места нужно выделить для типов с рекурсивным определением, компилятор выдаёт ошибку с этим полезным предложением: enum Message { Quit, Move { x: i32 , y: i32 }, Write( String ), ChangeColor( i32 , i32 , i32 ), } В данном предложении "перенаправление" означает, что вместо того, чтобы непосредственно хранить само значение, мы должны изменить структуру данных, так чтобы хранить его косвенно - хранить указатель на это значение. Поскольку Box является указателем, Rust всегда знает, сколько места нужно Box : размер указателя не меняется в зависимости от объёма данных, на которые он указывает. Это означает, что мы можем поместить Box внутрь экземпляра Cons вместо значения List напрямую. Box будет указывать на значение очередного List , который будет находиться в куче, а не внутри экземпляра Cons . Концептуально у нас все ещё есть список, созданный из списков, содержащих другие списки, но эта реализация теперь больше похожа на размещение элементов рядом друг с другом, а не внутри друг друга. Мы можем изменить определение перечисления List в листинге 15-2 и использование List в листинге 15-3 на код из листинга 15-5, который будет компилироваться: Файл: src/main.rs Листинг 15-5: Определение List , которое использует Box для того, чтобы иметь вычисляемый размер Cons требуется объём i32 плюс место для хранения данных указателя box. Nil не хранит никаких значений, поэтому ему нужно меньше места, чем Cons . Теперь мы знаем, что любое значение List займёт размер i32 плюс размер данных указателя box. Используя box, мы разорвали бесконечную рекурсивную цепочку, поэтому компилятор может определить размер, необходимый для хранения значения List . На рисунке 15-2 показано, как теперь выглядит Cons help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box
| ^^^^ ^ enum List { Cons( i32 , Box
Nil, } use crate::List::{Cons, Nil}; fn main () { let list = Cons( 1 , Box ::new(Cons( 2 , Box ::new(Cons( 3 , Box ::new(Nil)))))); } Cons i32 Box usize Рисунок 15-2: List , который не является бесконечно большим, потому что Cons хранит Box . Box-ы обеспечивают только перенаправление и выделение в куче; у них нет никаких других специальных возможностей, подобных тем, которые мы увидим у других типов умных указателей. У них также нет накладных расходов на производительность, которые несут эти специальные возможности, поэтому они могут быть полезны в таких случаях, как cons list, где перенаправление - единственная функция, которая нам нужна. В главе 17 мы также рассмотрим другие случаи использования box. Тип Box является умным указателем, поскольку он реализует трейт Deref , который позволяет обрабатывать значения Box как ссылки. Когда значение Box выходит из области видимости, данные кучи, на которые указывает box, также очищаются благодаря реализации типажа Drop . Эти два трейта будут ещё более значимыми для функциональности, предоставляемой другими типами умных указателей, которые мы обсудим в оставшейся части этой главы. Давайте рассмотрим эти два типажа более подробно. Обращение с умными указателями как с обычными ссылками с помощью Deref типажа Используя трейт Deref , вы можете изменить поведение оператора разыменования * (не путать с операторами умножения или глобального подключения). Реализовав Deref таким образом, что умный указатель может рассматриваться как обычная ссылка, вы можете писать код, оперирующий ссылками, а также использовать этот код с умными указателями. Давайте сначала посмотрим, как работает оператор разыменования с обычными ссылками. Затем мы попытаемся определить пользовательский тип, который ведёт себя как Box и посмотрим, почему оператор разыменования не работает как ссылка для нового объявленного типа. Мы рассмотрим, как реализация типажа Deref делает возможным работу умных указателей аналогично ссылкам. Затем посмотрим на разыменованное приведение (deref coercion) в Rust и как оно позволяет работать с любыми ссылками или умными указателями. Примечание: есть одна большая разница между типом MyBox , который мы собираемся создать и реальным Box : наша версия не будет хранить свои данные в куче. В примере мы сосредоточимся на типаже Deref , поэтому менее важно то, где данные хранятся, чем поведение подобное указателю. Следуя за указателем на значение Обычная ссылка - это разновидность указателя, а указатель можно рассматривать как своеобразную стрелочку направляющую к значению, хранящемуся в другом месте. В листинге 15-6 мы создаём ссылку на значение i32 , а затем используем оператор разыменования для перехода от ссылки к значению: Файл: src/main.rs Листинг 15-6: Использование оператора разыменования для следования по ссылке к значению i32 Переменной x присвоено значение 5 типа i32 . Мы установили в качестве значения y ссылку на x . Мы можем утверждать, что значение x равно 5 . Однако, если мы хотим сделать утверждение о значении в y , мы должны использовать *y , чтобы перейти по fn main () { let x = 5 ; let y = &x; assert_eq! ( 5 , x); assert_eq! ( 5 , *y); } ссылке к значению, на которое она указывает (таким образом, происходит разыменование), для того чтобы компилятор при сравнении мог использовать фактическое значение. Как только мы разыменуем y , мы получим доступ к целочисленному значению, на которое указывает y , которое и будем сравнивать с 5 Если бы мы попытались написать assert_eq!(5, y); , то получили ошибку компиляции: Сравнение числа и ссылки на число не допускается, потому что они различных типов. Мы должны использовать оператор разыменования, чтобы перейти по ссылке на значение, на которое она указывает. Использование Box Мы можем переписать код в листинге 15-6, чтобы использовать Box вместо ссылки; оператор разыменования, используемый для Box в листинге 15-7, работает так же, как оператор разыменования, используемый для ссылки в листинге 15-6: Файл: src/main.rs $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0277]: can't compare `{integer}` with `&{integer}` --> src/main.rs:6:5 | 6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` | = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` = help: the following other types implement trait `PartialEq = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. error: could not compile `deref-example` due to previous error fn main () { let x = 5 ; let y = Box ::new(x); assert_eq! ( 5 , x); assert_eq! ( 5 , *y); } Листинг 15-7: Использование оператора разыменования с типом Box Разница между листингом 15-7 и листингом 15-6 состоит в том, что здесь мы устанавливаем y на экземпляр box, указывающий на значение x , а не ссылкой, указывающей на значение x . В последнем утверждении мы можем использовать оператор разыменования, чтобы проследовать за указателем box-а так же, как мы это делали когда y была ссылкой. Далее мы рассмотрим, что особенного у типа Box , что позволяет нам использовать оператор разыменования, определяя наш собственный тип Box Определение собственного умного указателя Давайте создадим умный указатель, похожий на тип Box предоставляемый стандартной библиотекой, чтобы понять как поведение умных указателей отличается от поведения обычной ссылки. Затем мы рассмотрим вопрос, как добавить возможность использовать оператор разыменования. Тип Box в конечном итоге определяется как структура кортежа с одним элементом, поэтому в листинге 15-8 аналогичным образом определяется MyBox . Мы также определим функцию new , чтобы она соответствовала функции new , определённой в Box Файл: src/main.rs Листинг 15-8: Определение типа MyBox Мы определяем структуру с именем MyBox и объявляем обобщённый параметр T , потому что мы хотим, чтобы наш тип хранил значения любого типа. Тип MyBox является структурой кортежа с одним элементом типа T . Функция MyBox::new принимает один параметр типа T и возвращает экземпляр MyBox , который содержит переданное значение. Давайте попробуем добавить функцию main из листинга 15-7 в листинг 15-8 и изменим её на использование типа MyBox , который мы определили вместо Box . Код в листинге 15-9 не будет компилироваться, потому что Rust не знает, как разыменовывать MyBox Файл: src/main.rs struct MyBox (x: T) -> MyBox MyBox(x) } } Листинг 15-9. Попытка использовать MyBox таким же образом, как мы использовали ссылки и Box Вот результат ошибки компиляции: Наш тип MyBox не может быть разыменован, потому что мы не реализовали эту возможность. Чтобы включить разыменование с помощью оператора * , мы реализуем типаж Deref Трактование типа как ссылки реализуя типаж Deref Как обсуждалось в разделе “Реализация трейта для типа” Главы 10, для реализации типажа нужно предоставить реализации требуемых методов типажа. Типаж Deref , предоставляемый стандартной библиотекой требует от нас реализации одного метода с именем deref , который заимствует self и возвращает ссылку на внутренние данные. Листинг 15-10 содержит реализацию Deref добавленную к определению MyBox : Файл: src/main.rs Листинг 15-10: Реализация Deref для типа MyBox fn main () { let x = 5 ; let y = MyBox::new(x); assert_eq! ( 5 , x); assert_eq! ( 5 , *y); } $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 | 14 | assert_eq!(5, *y); | ^^ For more information about this error, try `rustc --explain E0614`. error: could not compile `deref-example` due to previous error use std::ops::Deref; impl MyBox Target = T; fn deref (& self ) -> &Self::Target { & self 0 } } Синтаксис type Target = T; определяет связанный тип для использования у типажа Deref . Связанные типы - это немного другой способ объявления обобщённого параметра, но пока вам не нужно о них беспокоиться; мы рассмотрим их более подробно в главе 19. Мы заполним тело метода deref оператором &self.0 , чтобы deref вернул ссылку на значение, к которому мы хотим получить доступ с помощью оператора * ; вспомним из раздела "Using Tuple Structs without Named Fields to Create Different Types" главы 5, что .0 получает доступ к первому значению в кортежной структуре. Функция main в листинге 15-9, которая вызывает * для значения MyBox , теперь компилируется, и проверки проходят! Без типажа Deref компилятор может только разыменовывать & ссылки. Метод deref даёт компилятору возможность принимать значение любого типа, реализующего Deref и вызывать метод deref чтобы получить ссылку & , которую он знает, как разыменовывать. Когда мы ввели *y в листинге 15-9, Rust фактически выполнил за кулисами такой код: Rust заменяет оператор * вызовом метода deref и затем простое разыменование, поэтому нам не нужно думать о том, нужно ли нам вызывать метод deref . Эта функция Rust позволяет писать код, который функционирует одинаково, независимо от того, есть ли у нас обычная ссылка или тип, реализующий типаж Deref Причина, по которой метод deref возвращает ссылку на значение, и что простое разыменование вне круглых скобок в *(y.deref()) все ещё необходимо, связана с системой владения. Если бы метод deref возвращал значение напрямую, а не ссылку на него, значение переместилось бы из self . Мы не хотим передавать владение внутренним значением внутри MyBox в этом случае и в большинстве случаев, когда мы используем оператор разыменования. Обратите внимание, что оператор * заменён вызовом метода deref , а затем вызовом оператора * только один раз, каждый раз, когда мы используем * в коде. Поскольку замена оператора * не повторяется бесконечно, мы получаем данные типа i32 , которые соответствуют 5 в assert_eq! листинга 15-9. Неявные разыменованные приведения с функциями и методами Разыменованное приведение преобразует ссылку на тип, который реализует признак Deref , в ссылку на другой тип. Например, deref coercion может преобразовать &String в &str , потому что String реализует признак Deref , который возвращает &str . Deref coercion - это удобный механизм, который Rust использует для аргументов функций и *(y.deref()) методов, и работает только для типов, реализующих признак Deref . Это происходит автоматически, когда мы передаём в качестве аргумента функции или метода ссылку на значение определённого типа, которое не соответствует типу параметра в определении функции или метода. В результате серии вызовов метода deref тип, который мы передали, преобразуется в тип, необходимый для параметра. Разыменованное приведение было добавлено в Rust, так что программистам, пишущим вызовы функций и методов, не нужно добавлять множество явных ссылок и разыменований с помощью использования & и * . Функциональность разыменованного приведения также позволяет писать больше кода, который может работать как с ссылками, так и с умными указателями. Чтобы увидеть разыменованное приведение в действии, давайте воспользуемся типом MyBox определённым в листинге 15-8, а также реализацию Deref добавленную в листинге 15-10. Листинг 15-11 показывает определение функции, у которой есть параметр типа срез строки: Файл: src/main.rs |