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

  • Листинг 15-3: Использование перечисления List для хранения списка

  • Листинг 15-4: Ошибка, которую мы получаем при попытке определить рекурсивное перечисление

  • Вычисление размера нерекурсивного типа

  • Рисунок 15-1: Бесконечный List, состоящий из нескончаемого числа вариантов ConsИспользование

  • Листинг 15-5: Определение List, которое использует Box для того, чтобы иметь вычисляемый размер

  • Рисунок 15-2: List, который не является бесконечно большим, потому что Cons хранит Box.

  • Обращение с умными указателями как с обычными ссылками с помощью

  • Следуя за указателем на значение

  • Листинг 15-6: Использование оператора разыменования для следования по ссылке к значению

  • Использование

  • Листинг 15-7: Использование оператора разыменования с типом

  • Определение собственного умного указателя

  • Листинг 15-8: Определение типа

  • Листинг 15-9. Попытка использовать MyBox таким же образом, как мы использовали ссылки и

  • Трактование типа как ссылки реализуя типаж

  • Листинг 15-10: Реализация Deref для типа

  • Неявные разыменованные приведения с функциями и методами

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


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница39 из 62
    1   ...   35   36   37   38   39   40   41   42   ...   62
    Листинг 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`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others
    = 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
    (T); impl
    MyBox { fn new
    (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
    Deref for
    MyBox { type
    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
    1   ...   35   36   37   38   39   40   41   42   ...   62


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