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

  • Листинг 15-12: Вызов hello со ссылкой на значение MyBox , которое работает из-за разыменованного приведения

  • Листинг 15-13: Код, который нам пришлось бы написать, если бы в Rust не было разыменованного приведения ссылок

  • Как разыменованное приведение взаимодействует с изменяемостью

  • Запуск кода при очистке с помощью типажа

  • Листинг 15-14: Структура CustomSmartPointer, реализующая типаж Drop, куда мы поместим наш код очистки

  • Раннее удаление значения с помощью

  • Листинг 15-15: Попытка вызвать метод drop из трейта Drop вручную для досрочной очистки

  • Листинг 15-16: Вызов std::mem::drop для принудительного удаления значения до того, как оно выйдет из области видимости

  • , умный указатель с подсчётом ссылок

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

  • Рисунок 15-3: Два списка, b и c, делят владение над третьим списком

  • Листинг 15-17: Демонстрация того, что нельзя иметь два списка, использующих Box , которые пытаются совместно владеть третьим списком

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


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница40 из 62
    1   ...   36   37   38   39   40   41   42   43   ...   62
    Листинг 15-11: Функция
    hello
    имеющая параметр
    name
    типа
    &str
    Можно вызвать функцию hello со срезом строки в качестве аргумента, например hello("Rust");
    . Разыменованное приведение делает возможным вызов hello со ссылкой на значение типа
    MyBox
    , как показано в листинге 15-12.
    Файл: src/main.rs
    Листинг 15-12: Вызов
    hello
    со ссылкой на значение
    MyBox
    , которое работает из-за
    разыменованного приведения
    Здесь мы вызываем функцию hello с аргументом
    &m
    , который является ссылкой на значение
    MyBox
    . Поскольку мы реализовали типаж
    Deref для
    MyBox
    в листинге 15-10, то Rust может преобразовать
    &MyBox
    в
    &String вызывая deref
    Стандартная библиотека предоставляет реализацию типажа
    Deref для типа
    String
    ,
    которая возвращает срез строки, это описано в документации API типажа
    Deref
    . Rust снова вызывает deref
    , чтобы превратить
    &String в
    &str
    , что соответствует определению функции hello fn hello
    (name: &
    str
    ) { println!
    (
    "Hello, {name}!"
    );
    } fn main
    () { let m = MyBox::new(
    String
    ::from(
    "Rust"
    )); hello(&m);
    }

    Если бы Rust не реализовал разыменованное приведение, мы должны были бы написать код в листинге 15-13 вместо кода в листинге 15-12 для вызова метода hello со значением типа
    &MyBox
    Файл: src/main.rs
    Листинг 15-13: Код, который нам пришлось бы написать, если бы в Rust не было разыменованного
    приведения ссылок
    (*m)
    разыменовывает
    MyBox
    в
    String
    . Затем
    &
    и
    [...]
    принимают фрагмент строки
    String
    , равный полной строке для соответствия сигнатуре hello
    . Этот код без deref-согласований сложнее читать, писать и понимать, поскольку в нем задействованы все эти символы. Deref coercion позволяет Rust автоматически обрабатывать эти преобразования.
    Когда типаж
    Deref определён для задействованных типов, Rust проанализирует типы и будет использовать
    Deref::deref столько раз, сколько необходимо, чтобы получить ссылку, соответствующую типу параметра. Количество раз, которое нужно вставить
    Deref::deref определяется во время компиляции, поэтому использование разыменованного приведения не имеет накладных расходов во время выполнения!
    Как разыменованное приведение взаимодействует с изменяемостью
    Подобно тому, как вы используете типаж
    Deref для переопределения оператора
    *
    у неизменяемых ссылок, вы можете использовать типаж
    DerefMut для переопределения оператора
    *
    у изменяемых ссылок.
    Rust выполняет разыменованное приведение, когда находит типы и реализации типажей в трёх случаях:
    Из типа
    &T
    в тип
    &U
    когда верно
    T: Deref
    Из типа
    &mut T
    в тип
    &mut U
    когда верно
    T: DerefMut
    Из типа
    &mut T
    в тип
    &U
    когда верно
    T: Deref
    Первые два случая идентичны друг другу, за исключением того, что второй реализует изменяемость. В первом случае говорится, что если у вас есть
    &T
    , а
    T
    реализует
    Deref для некоторого типа
    U
    , вы сможете прозрачно получить
    &U
    . Во втором случае говорится, что такое же разыменованное приведение происходит и для изменяемых ссылок.
    fn main
    () { let m = MyBox::new(
    String
    ::from(
    "Rust"
    )); hello(&(*m)[..]);
    }

    Третий случай хитрее: Rust также приводит изменяемую ссылку к неизменяемой. Но обратное не представляется возможным: неизменяемые ссылки никогда не приводятся к изменяемым ссылкам. Из-за правил заимствования, если у вас есть изменяемая ссылка,
    эта изменяемая ссылка должна быть единственной ссылкой на данные (в противном случае программа не будет компилироваться). Преобразование одной изменяемой ссылки в неизменяемую ссылку никогда не нарушит правила заимствования.
    Преобразование неизменяемой ссылки в изменяемую ссылку потребует наличия только одной неизменяемой ссылки на эти данные, и правила заимствования не гарантируют этого. Следовательно, Rust не может сделать предположение, что преобразование неизменяемой ссылки в изменяемую ссылку возможно.

    Запуск кода при очистке с помощью типажа Drop
    Вторым важным типажом умного указателя является Drop, который позволяет регулировать, что происходит, когда значение вот-вот выйдет из области видимости. Вы можете реализовать типаж Drop для любого типа, а также использовать этот код для высвобождения ресурсов, таких как файлы или сетевые соединения.
    Мы рассматриваем
    Drop в контексте умных указателей, потому что функциональность свойства
    Drop практически всегда используется при реализации умного указателя.
    Например, при сбросе
    Box
    происходит деаллокация пространства на куче, на которое указывает box.
    В некоторых языках для некоторых типов программист должен вызывать код для освобождения памяти или ресурсов каждый раз, когда он завершает использование экземпляров этих типов. Примерами могут служить дескрипторы файлов, сокеты или блокировки. Если забыть об этом, система окажется перегруженной и может упасть. В
    Rust вы можете указать, что определённый фрагмент кода должен выполняться всякий раз, когда значение выходит из области видимости, и компилятор автоматически будет его вставлять. Как следствие, вам не нужно заботиться о размещении кода очистки везде в программе, где завершается работа экземпляра определённого типа - утечки ресурсов все равно не будет!
    Вы можете задать определённую логику, которая будет выполняться, когда значение выходит за пределы области видимости, реализовав признак
    Drop
    . Типаж
    Drop требует от вас реализации одного метода drop
    , который принимает изменяемую ссылку на self
    . Чтобы увидеть, когда Rust вызывает drop
    , давайте реализуем drop с помощью операторов println!
    В листинге 15-14 показана структура
    CustomSmartPointer
    , единственной уникальной функциональностью которой является печать
    Dropping CustomSmartPointer!
    , когда экземпляр выходит из области видимости, чтобы показать, когда Rust выполняет функцию drop
    Файл: src/main.rs

    Листинг 15-14: Структура
    CustomSmartPointer
    , реализующая типаж
    Drop
    , куда мы поместим наш код
    очистки
    Типаж
    Drop включён в прелюдию, поэтому нам не нужно вводить его в область видимости. Мы реализуем типаж
    Drop для
    CustomSmartPointer и реализуем метод drop
    ,
    который будет вызывать println!
    . Тело функции drop
    - это место, где должна располагаться вся логика, которую вы захотите выполнять, когда экземпляр вашего типа выйдет из области видимости. Мы печатаем здесь текст, чтобы наглядно продемонстрировать, когда Rust вызовет drop
    В main мы создаём два экземпляра
    CustomSmartPointer и затем печатаем
    CustomSmartPointers created
    . В конце main наши экземпляры
    CustomSmartPointer выйдут из области видимости и Rust вызовет код, который мы добавили в метод drop
    ,
    который и напечатает наше окончательное сообщение. Обратите внимание, что нам не нужно вызывать метод drop явно.
    Когда мы запустим эту программу, мы увидим следующий вывод:
    Rust автоматически вызывал drop в момент выхода наших экземпляров из области видимости, тем самым выполнив заданный нами код. Переменные ликвидируются в обратном порядке их создания, поэтому d
    была ликвидирована до c
    . Цель этого примера - дать вам наглядное представление о том, как работает метод drop
    ; в struct
    CustomSmartPointer
    { data:
    String
    ,
    } impl
    Drop for
    CustomSmartPointer { fn drop
    (&
    mut self
    ) { println!
    (
    "Dropping CustomSmartPointer with data `{}`!"
    , self
    .data);
    }
    } fn main
    () { let c = CustomSmartPointer { data:
    String
    ::from(
    "my stuff"
    ),
    }; let d = CustomSmartPointer { data:
    String
    ::from(
    "other stuff"
    ),
    }; println!
    (
    "CustomSmartPointers created."
    );
    }
    $
    cargo run
    Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
    Running `target/debug/drop-example`
    CustomSmartPointers created.
    Dropping CustomSmartPointer with data `other stuff`!
    Dropping CustomSmartPointer with data `my stuff`!
    типичных случаях вы будете задавать код очистки, который должен выполнить ваш тип,
    а не печатать сообщение.
    Раннее удаление значения с помощью std::mem::drop
    К сожалению, отключение функции автоматического удаления с помощью drop является не простым. Отключение drop обычно не требуется; весь смысл типажа
    Drop том, чтобы о функции позаботились автоматически. Иногда, однако, вы можете захотеть очистить значение рано. Одним из примеров является использование интеллектуальных указателей, которые управляют блокировками: вы могли бы потребовать принудительный вызов метода drop который снимает блокировку, чтобы другой код в той же области видимости мог получить блокировку. Rust не позволяет вызвать метод типажа
    Drop вручную; вместо этого вы должны вызвать функцию std::mem::drop предоставляемую стандартной библиотекой, если хотите принудительно удалить значение до конца области видимости.
    Если попытаться вызвать метод drop типажа
    Drop вручную, изменяя функцию main листинга 15-14 так, как показано в листинге 15-15, мы получим ошибку компилятора:
    Файл: src/main.rs
    Листинг 15-15: Попытка вызвать метод
    drop
    из трейта
    Drop
    вручную для досрочной очистки
    Когда мы попытаемся скомпилировать этот код, мы получим ошибку:
    fn main
    () { let c = CustomSmartPointer { data:
    String
    ::from(
    "some data"
    ),
    }; println!
    (
    "CustomSmartPointer created."
    ); c.
    drop
    (); println!
    (
    "CustomSmartPointer dropped before the end of main."
    );
    }
    $
    cargo run
    Compiling drop-example v0.1.0 (file:///projects/drop-example) error[E0040]: explicit use of destructor method
    -->
    src/main.rs:16:7
    |
    16 | c.drop();
    | --^^^^--
    | | |
    | | explicit destructor calls not allowed
    | help: consider using `drop` function: `drop(c)`
    For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` due to previous error

    Это сообщение об ошибке говорит, что мы не можем явно вызывать drop
    . В сообщении об ошибке используется термин деструктор (destructor), который является общим термином программирования для функции, которая очищает экземпляр. Деструктор
    аналогичен конструктору, который создаёт экземпляр. Функция drop в Rust является определённым деструктором.
    Rust не позволяет обращаться к drop напрямую, потому что он все равно автоматически вызовет drop в конце main
    . Это вызвало бы ошибку double free, потому что в этом случае
    Rust попытался бы дважды очистить одно и то же значение.
    Невозможно отключить автоматическую подстановку вызова drop
    , когда значение выходит из области видимости, и нельзя вызвать метод drop напрямую. Поэтому, если нам нужно принудительно избавиться от значения раньше времени, следует использовать функцию std::mem::drop
    Функция std::mem::drop отличается от метода drop трейта
    Drop
    . Мы вызываем её,
    передавая в качестве аргумента значение, которое хотим принудительно уничтожить.
    Функция находится в прелюдии, поэтому мы можем изменить main в листинге 15-15 так,
    чтобы вызвать функцию drop
    , как показано в листинге 15-16:
    Файл: src/main.rs
    Листинг 15-16: Вызов
    std::mem::drop
    для принудительного удаления значения до того, как оно выйдет из
    области видимости
    Выполнение данного кода выведет следующий результат::
    Текст
    Dropping CustomSmartPointer with data some data
    !
    , напечатанный между
    CustomSmartPointer created.
    и текстом
    CustomSmartPointer dropped before the end of main.
    , показывает, что код метода drop вызывается для удаления c
    в этой точке.
    fn main
    () { let c = CustomSmartPointer { data:
    String
    ::from(
    "some data"
    ),
    }; println!
    (
    "CustomSmartPointer created."
    ); drop
    (c); println!
    (
    "CustomSmartPointer dropped before the end of main."
    );
    }
    $
    cargo run
    Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
    Running `target/debug/drop-example`
    CustomSmartPointer created.
    Dropping CustomSmartPointer with data `some data`!
    CustomSmartPointer dropped before the end of main.

    Вы можете использовать код, указанный в реализации типажа
    Drop
    , чтобы сделать очистку удобной и безопасной: например, вы можете использовать её для создания своего собственного менеджера памяти! С помощью типажа
    Drop и системы владения
    Rust не нужно специально заботиться о том, чтобы освобождать ресурсы, потому что
    Rust делает это автоматически.
    Также не нужно беспокоиться о проблемах, возникающих в результате случайной очистки значений, которые всё ещё используются: система владения, которая гарантирует, что ссылки всегда действительны, также гарантирует, что drop вызывается только один раз, когда значение больше не используется.
    После того, как мы познакомились с
    Box
    и характеристиками умных указателей,
    познакомимся с её другими умными указателями, определёнными в стандартной библиотеке.

    Rc
    , умный указатель с подсчётом ссылок
    В большинстве ситуаций владение является однозначным: вы точно знаете, какая переменная владеет данным значением. Однако бывают случаи, когда у одного значения может быть несколько владельцев. Например, в Графовых структурах может быть несколько рёбер, указывающих на один и тот же узел — таким образом, этот узел становится фактически собственностью всех этих рёбер. Узел не подлежит удалению, за исключением тех случаев, когда на него не указывает ни одно ребро и, соответственно, у него нет владельцев.
    Вы должны включить множественное владение явно, используя тип Rust
    Rc
    , который является аббревиатурой для подсчёта ссылок. Тип
    Rc
    отслеживает количество ссылок на значение, чтобы определить, используется ли оно ещё. Если ссылок на значение нет,
    значение может быть очищено и при этом ни одна ссылка не станет недействительной.
    Представьте себе
    Rc
    как телевизор в гостиной. Когда один человек входит, чтобы смотреть телевизор, он включает его. Другие могут войти в комнату и посмотреть телевизор. Когда последний человек покидает комнату, он выключает телевизор, потому что он больше не используется. Если кто-то выключит телевизор во время его просмотра другими, то оставшиеся телезрители устроят шум!
    Тип
    Rc
    используется, когда мы хотим разместить в куче некоторые данные для чтения несколькими частями нашей программы и не можем определить во время компиляции, какая из частей завершит использование данных последней. Если бы мы знали, какая часть завершит использование последней то, мы могли бы сделать эту часть владельцем данных и вступили бы в силу обычные правила владения, применяемые во время компиляции.
    Обратите внимание, что
    Rc
    используется только в одно поточных сценариях. Когда мы обсудим конкурентность в главе 16, мы рассмотрим, как выполнять подсчёт ссылок во много поточных программах.
    Использование Rc для совместного использования данных
    Давайте вернёмся к нашему примеру с cons списком в листинге 15-5. Напомним, что мы определили его с помощью типа
    Box
    . В этот раз мы создадим два списка, оба из которых будут владеть третьим списком. Концептуально это похоже на рисунок 15-3:
    b
    3 5 a
    10
    Nil c
    4
    Рисунок 15-3: Два списка,
    b
    и
    c
    , делят владение над третьим списком,
    a
    Мы создадим список a
    , содержащий 5 и затем 10. Затем мы создадим ещё два списка: b
    начинающийся с 3 и c
    начинающийся с 4. Оба списка b
    и c
    затем продолжать первый список a
    , содержащий 5 и 10. Другими словами, оба списка будут разделять первый список, содержащий 5 и 10.
    Попытка реализовать этот сценарий, используя определение
    List с типом
    Box
    не будет работать, как показано в листинге 15-17:
    Файл: src/main.rs
    Листинг 15-17: Демонстрация того, что нельзя иметь два списка, использующих
    Box
    , которые пытаются
    совместно владеть третьим списком
    При компиляции этого кода, мы получаем эту ошибку:
    enum
    List
    {
    Cons(
    i32
    ,
    Box
    ),
    Nil,
    } use crate::List::{Cons, Nil}; fn main
    () { let a = Cons(
    5
    ,
    Box
    ::new(Cons(
    10
    ,
    Box
    ::new(Nil)))); let b = Cons(
    3
    ,
    Box
    ::new(a)); let c = Cons(
    4
    ,
    Box
    ::new(a));
    }

    Варианты
    Cons владеют данными, которые они содержат, поэтому, когда мы создаём список b
    , то a
    перемещается в b
    , а b
    становится владельцем a
    . Затем, мы пытаемся использовать a
    снова при создании c
    , но нам не разрешают, потому что a
    был перемещён.
    Мы могли бы изменить определение
    Cons
    , чтобы вместо этого хранить ссылки, но тогда нам пришлось бы указывать параметры времени жизни. Указывая параметры времени жизни, мы бы указали, что каждый элемент в списке будет жить как минимум столько же,
    сколько и весь список. Это относится к элементам и спискам в листинге 15.17, но не во всех сценариях.
    Вместо этого мы изменим наше определение типа
    List так, чтобы использовать
    Rc
    вместо
    Box
    , как показано в листинге 15-18. Каждый вариант
    Cons теперь будет содержать значение и тип
    Rc
    , указывающий на
    List
    . Когда мы создадим b
    то,
    вместо того чтобы стал владельцем a
    , мы будем клонировать
    Rc
    который содержит a
    , тем самым увеличивая количество ссылок с единицы до двойки и позволяя переменным a
    и b
    разделять владение на данные в типе
    Rc
    . Мы также клонируем a
    при создании c
    , увеличивая количество ссылок с двух до трёх. Каждый раз,
    когда мы вызываем
    Rc::clone
    , счётчик ссылок на данные внутри
    Rc
    будет увеличиваться и данные не будут очищены, если на них нет нулевых ссылок.
    Файл: src/main.rs
    $
    cargo run
    Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: `a`
    -->
    src/main.rs:11:30
    |
    9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    | - move occurs because `a` has type `List`, which does not implement the `Copy` trait
    10 | let b = Cons(3, Box::new(a));
    | - value moved here
    11 | let c = Cons(4, Box::new(a));
    | ^ value used here after move
    For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error

    1   ...   36   37   38   39   40   41   42   43   ...   62


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