Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 18-28: Комбинирование нескольких шаблонов с условием сопоставления Условие сопоставления гласит, что ветка совпадает, только если значение x равно 4 , 5 или 6 , и если y равно true . Когда этот код выполняется, шаблон первой ветки совпадает, потому что x равно 4 , но условие сопоставления if y равно false, поэтому первая ветка не выбрана. Код переходит ко второй ветке, которая совпадает, и эта программа печатает no . Причина в том, что условие if применяется ко всему шаблону 4 | 5 | 6 , а не только к последнему значению 6 . Другими словами, приоритет условия сопоставления по отношению к шаблону ведёт себя так: а не так: После запуска кода, старшинство в поведении становится очевидным: если условие сопоставления применялось бы только к конечному значению в списке, указанном с помощью оператора | , то ветка бы совпала и программа напечатала бы yes let x = 4 ; let y = false ; match x { 4 | 5 | 6 if y => println! ( "yes" ), _ => println! ( "no" ), } (4 | 5 | 6) if y => ... 4 | 5 | (6 if y) => ... Связывание @ Оператор at ( @ ) позволяет создать переменную, которая содержит значение, одновременно с тем, как мы проверяем, соответствует ли это значение шаблону. В листинге 18-29 показан пример, в котором мы хотим проверить, что перечисление Message::Hello со значением поля id находится в диапазоне 3..=7 . Но мы также хотим привязать такое значение к переменной id_variable , чтобы использовать его внутри кода данной ветки. Мы могли бы назвать эту переменную id , так же как поле, но для этого примера мы будем использовать другое имя. Листинг 18-29: Использование @ для привязывания значения в шаблоне, с одновременной его проверкой В этом примере будет напечатано Found an id in range: 5 . Указывая id_variable @ перед диапазоном 3..=7 , мы захватываем любое значение, попадающее в диапазон, одновременно проверяя, что это значение соответствует диапазону в шаблоне. Во второй ветке, где у нас в шаблоне указан только диапазон, код этой ветки не имеет переменной, которая содержит фактическое значение поля id . Значение поля id могло бы быть 10, 11 или 12, но код, соответствующий этому шаблону, не знает, чему оно равно. Код шаблона не может использовать значение из поля id , потому что мы не сохранили значение id в переменной. В последней ветке, где мы указали переменную без диапазона, у нас есть значение, доступное для использования в коде ветки, в переменной с именем id . Причина в том, что мы использовали упрощённый синтаксис полей структуры. Но мы не применяли никакого сравнения со значением в поле id в этой ветке, как мы это делали в первых двух ветках: любое значение будет соответствовать этому шаблону. Использование @ позволяет проверять значение и сохранять его в переменной в пределах одного шаблона. enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3 ..= 7 , } => println! ( "Found an id in range: {}" , id_variable), Message::Hello { id: 10 ..= 12 } => { println! ( "Found an id in another range" ) } Message::Hello { id } => println! ( "Found some other id: {}" , id), } Итоги Шаблоны Rust очень помогают различать разные виды данных. При использовании их в выражениях match , Rust гарантирует, что ваши шаблоны охватывают все возможные значения, потому что иначе ваша программа не скомпилируется. Шаблоны в операторах let и параметрах функций делают такие конструкции более полезными, позволяя разбивать элементы на более мелкие части, одновременно присваивая их значения переменным. Мы можем создавать простые или сложные шаблоны в соответствии с нашими потребностями. Далее, в предпоследней главе книги, мы рассмотрим некоторые продвинутые аспекты различных возможностей Rust. Расширенные возможности К настоящему времени вы изучили наиболее часто используемые части языка программирования Rust. Прежде чем мы сделаем ещё один проект в главе 20, мы рассмотрим несколько аспектов языка с которыми вы можете сталкиваться время от времени. Можете использовать эту главу в качестве справки, когда столкнётесь с неизвестными возможностями Rust. Возможности, которые вы научитесь использовать в этой главе, полезны в специальных ситуациях. Хотя возможно вы не часто их встретите, мы хотим быть уверены, что у вас есть понимание всех возможностей, которые может предложить Rust. В этой главе мы рассмотрим: Небезопасный Rust: как отказаться от некоторых гарантий Rust и взять на себя ответственность за их ручное соблюдение Продвинутые типажи: ассоциированные типы, параметры типа по умолчанию, полностью квалифицированный синтаксис, супер-типажи и шаблон создания (newtype) по отношению к типажам Расширенные типы: больше о шаблоне newtype, псевдонимах типа, тип never и типы динамических размеров Расширенные функции и замыкания: указатели функций и возврат замыканий Макросы: способы определения кода, который определяет большую часть кода во время компиляции Это набор возможностей Rust для всех! Давайте погрузимся в него! Unsafe Rust Во всех предыдущих главах этой книги мы обсуждали код на Rust, безопасность памяти в котором гарантируется во время компиляции. Однако внутри Rust скрывается другой язык - небезопасный Rust, который не обеспечивает безопасной работы с памятью. Этот язык называется unsafe Rust и работает также как и первый, но предоставляет вам дополнительные возможности. Небезопасный Rust существует, потому что по своей природе статический анализ является консервативным. Когда компилятор пытается определить, поддерживает ли код некоторые гарантии или нет, то лучше отклонить некоторые действительные программы, которые корректны, чем принимать некоторые программы, которые ошибочны. Бывают случаи, когда ваш код может быть правильным, но Rust считает, что это не так. В этих случаях вы можете использовать небезопасный код, чтобы сообщить компилятору: «поверь мне, я знаю, что делаю». Недостатком является то, что вы используете его на свой страх и риск. Если вы используете небезопасный код неправильно, то могут появиться проблемы из-за небезопасной работы с памятью, такие как разыменование нулевого указателя. Другая причина, по которой у Rust есть небезопасное альтер эго, заключается в том, что по существу аппаратное обеспечение компьютера небезопасно. Если Rust не позволял бы вам выполнять небезопасные операции, вы не могли бы выполнять определённые задачи. Rust должен позволить вам использовать системное, низкоуровневое программирование, такое как прямое взаимодействие с операционной системой, или даже написание вашей собственной операционной системы. Возможность написания низкоуровневого, системного кода является одной из целей языка. Давайте рассмотрим, что и как можно делать с небезопасным Rust. Небезопасные сверхспособности Чтобы переключиться на небезопасный Rust, используйте ключевое слово unsafe и начните новый блок, содержащий небезопасный код. Вы можете совершить пять действий в небезопасном Rust коде, которые называются небезопасными сверхспособностями и которые вы не можете выполнить в безопасном Rust. Эти сверхспособности включают в себя следующие возможности: Разыменование сырого указателя Вызов небезопасной функции или небезопасного метода Доступ или изменение изменяемой статической переменной Реализация небезопасного типажа Доступ к полям в union Важно понимать, что unsafe не отключает проверку заимствования или любые другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она всё равно будет проверена. Единственное, что делает ключевое слово unsafe - даёт вам доступ к этим четырём возможностям, безопасность работы с памятью в которых не проверяет компилятор. Вы по-прежнему получаете некоторую степень безопасности внутри небезопасного блока. Кроме того, unsafe не означает, что код внутри этого блока является неизбежно опасным или он точно будет иметь проблемы с безопасностью памяти: цель состоит в том, что вы, как программист, гарантируете, что код внутри блока unsafe будет обращаться к действительной памяти корректным образом. Люди подвержены ошибкам и ошибки будут происходить, но требуя размещение этих четырёх небезопасных операции внутри блоков, помеченных как unsafe , вы будете знать, что любые ошибки, связанные с безопасностью памяти, будут находиться внутри unsafe блоков. Делайте unsafe блоки маленькими; вы будете благодарны себе за это позже, при исследовании ошибок с памятью. Чтобы максимально изолировать небезопасный код, рекомендуется заключить небезопасный код в безопасную абстракцию и предоставить безопасный API, который мы обсудим позже, когда будем обсуждать небезопасные функции и методы. Части стандартной библиотеки реализованы как проверенные, безопасные абстракции над небезопасным кодом. Оборачивание небезопасного кода в безопасную абстракцию предотвращает возможную утечку использования unsafe кода во всех местах, где вы или ваши пользователи могли бы захотеть напрямую использовать функциональность, реализованную unsafe кодом, потому что использование безопасной абстракции само безопасно. Давайте поговорим о каждой из четырёх небезопасных сверх способностей, и по ходу дела рассмотрим некоторые абстракции, которые обеспечивают безопасный интерфейс для небезопасного кода. Разыменование сырых указателей В главе 4 раздела "Недействительные ссылки" мы упоминали, что компилятор гарантирует, что ссылки всегда действительны. Небезопасный Rust имеет два новых типа, называемых сырыми указателями (raw pointers), которые похожи на ссылки. Как и в случае ссылок, сырые указатели могут быть неизменяемыми или изменяемыми и записываться как *const T и *mut T соответственно. Звёздочка не является оператором разыменования; это часть имени типа. В контексте сырых указателей неизменяемый (immutable) означает, что указателю нельзя напрямую присвоить что-то после того как он разыменован. В отличие от ссылок и умных указателей, сырые указатели: могут игнорировать правила заимствования и иметь неизменяемые и изменяемые указатели, или множество изменяемых указателей на одну и ту же область памяти не гарантируют что ссылаются на действительную память могут быть null не реализуют автоматическую очистку памяти Отказавшись от этих гарантий, вы можете обменять безопасность на большую производительность или возможность взаимодействия с другим языком или оборудованием, где гарантии Rust не применяются. В листинге 19-1 показано, как создать неизменяемый и изменяемый сырой указатель из ссылок. Листинг 19-1. Создание необработанных указателей из ссылок Обратите внимание, что мы не используем ключевое слово unsafe в этом коде. Можно создавать сырые указатели в безопасном коде; мы просто не можем разыменовывать сырые указатели за пределами небезопасного блока, как вы увидите чуть позже. Мы создали сырые указатели, используя as для приведения неизменяемой и изменяемой ссылки к соответствующим им типам сырых указателей. Поскольку мы создали их непосредственно из ссылок, которые гарантированно являются действительными, мы знаем, что эти конкретные сырые указатели являются действительными, но мы не можем делать такое же предположение о любом сыром указателе. Далее мы создадим сырой указатель, в достоверности которого мы не можем быть уверены. В листинге 19-2 показано, как создать сырой указатель на произвольный адрес памяти. Результат попытки использовать произвольную память не определён (undefined): по этому адресу могут быть данные или их может не быть, компилятор может оптимизировать код так, что не будет кода доступа к памяти или программа может выдать ошибку сегментации при выполнении. Обычно нет веских причин для написания такого кода, но это возможно. Листинг 19-2: Создание сырого указателя на произвольный адрес памяти Напомним, что можно создавать сырые указатели в безопасном коде, но нельзя разыменовывать сырые указатели и читать данные, на которые они указывают. В листинге 19-3 мы используем оператор разыменования * для сырого указателя, который требует unsafe блока. let mut num = 5 ; let r1 = &num as * const i32 ; let r2 = & mut num as * mut i32 ; let address = 0x012345usize ; let r = address as * const i32 ; Листинг 19-3: Разыменование сырых указателей внутри unsafe блока Создание указателей безопасно. Только при попытке доступа к объекту по адресу в указателе мы можем получить недопустимое значение. Также обратите внимание, что в примерах кода 19-1 и 19-3 мы создали *const i32 и *mut i32 , которые ссылаются на одну и ту же область памяти, где хранится num . Если мы попытаемся создать неизменяемую и изменяемую ссылку на num вместо сырых указателей, такой код не скомпилируется, т.к. будут нарушены правила заимствования, запрещающие наличие изменяемой ссылки одновременно с неизменяемыми ссылками. С помощью сырых указателей мы можем создать изменяемый указатель и неизменяемый указатель на одну и ту же область памяти и изменять данные с помощью изменяемого указателя, потенциально создавая эффект гонки данных. Будьте осторожны! С учётом всех этих опасностей, зачем тогда использовать сырые указатели? Одним из основных применений является взаимодействие с кодом C, как вы увидите в следующем разделе "Вызов небезопасной функции или метода" . Другой случай это создание безопасных абстракций, которые не понимает анализатор заимствований. Мы введём понятие небезопасных функций и затем рассмотрим пример безопасной абстракции, которая использует небезопасный код. Вызов небезопасной функции или метода Второй тип операции, который требует небезопасного блока - это вызов небезопасных функций. Небезопасные функции и методы выглядят точно так же, как обычные функции и методы, но они имеют дополнительное указание unsafe перед остальной частью определения. Ключевое слово unsafe в этом контексте указывает, что у функции есть требования, которые мы должны соблюдать при её вызове, потому что Rust не может гарантировать выполнение этих требований. Вызывая небезопасную функцию в unsafe блоке, мы говорим, что прочитали документацию по этой функции и несём ответственность за соблюдение её контрактов. Вот небезопасная функция с именем dangerous которая ничего не делает в своём теле: let mut num = 5 ; let r1 = &num as * const i32 ; let r2 = & mut num as * mut i32 ; unsafe { println! ( "r1 is: {}" , *r1); println! ( "r2 is: {}" , *r2); } Мы должны вызвать функцию dangerous в отдельном unsafe блоке. Если мы попробуем вызвать dangerous без unsafe блока, мы получим ошибку: Вставив unsafe блок вокруг нашего вызова dangerous , мы утверждаем, что мы прочитали документацию к функции, мы понимаем как её использовать правильно и мы убедились, что выполняем контракт функции. Тела небезопасных функций являются фактически unsafe блоками, поэтому для выполнения других небезопасных операций внутри небезопасной функции не нужно добавлять ещё один unsafe блок. Создание безопасных абстракций вокруг небезопасного кода Тот факт, что функция содержит небезопасный код, не означает, что мы должны пометить всю функцию как небезопасную. Фактически, упаковка небезопасного кода в безопасную функцию является обычной абстракцией. В качестве примера давайте изучим функцию из стандартной библиотеки split_at_mut , для которой требуется небезопасный код и исследуем, как мы могли бы её реализовать. Этот безопасный метод определён для изменяемых срезов: он берёт один срез и делит его на два, разделяя срез по индексу, указанному в качестве аргумента. В листинге 19.4 показано, как использовать split_at_mut unsafe fn dangerous () {} unsafe { dangerous(); } $ cargo run Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example) error[E0133]: call to unsafe function is unsafe and requires unsafe function or block --> src/main.rs:4:5 | 4 | dangerous(); | ^^^^^^^^^^^ call to unsafe function | = note: consult the function's documentation for information on how to avoid undefined behavior For more information about this error, try `rustc --explain E0133`. error: could not compile `unsafe-example` due to previous error Листинг 19-4: Использование безопасной функции split_at_mut Эту функцию нельзя реализовать, используя только безопасный Rust. Попытка реализации могла бы выглядеть примерно как в листинге 19-5, который не компилируется. Для простоты мы реализуем split_at_mut как функцию, а не как метод, и только для значений типа i32 , а не обобщённого типа T Листинг 19-5: Попытка реализации функции split_at_mut используя только безопасный Rust Эта функция сначала получает общую длину среза. Затем она проверяет(assert), что индекс, переданный в качестве параметра, находится в границах среза, сравнивая его с длиной. Assert означает, что если мы передадим индекс, который больше, чем длина среза, функция запаникует ещё до попытки использования этого индекса. Затем мы возвращаем два изменяемых фрагмента в кортеже: один от начала исходного фрагмента до mid индекса (не включая сам mid), а другой - от mid (включая сам mid) до конца фрагмента. При попытке скомпилировать код в листинге 19-5, мы получим ошибку. let mut v = vec! [ 1 , 2 , 3 , 4 , 5 , 6 ]; let r = & mut v[..]; let (a, b) = r.split_at_mut( 3 ); assert_eq! (a, & mut [ 1 , 2 , 3 ]); assert_eq! (b, & mut [ 4 , 5 , 6 ]); fn split_at_mut (values: & mut [ i32 ], mid: usize ) -> (& mut [ i32 ], & mut [ i32 ]) { let len = values.len(); assert! (mid <= len); (& mut values[..mid], & mut values[mid..]) } Анализатор заимствований Rust не может понять, что мы заимствуем различные части среза, он понимает лишь, что мы хотим осуществить заимствование частей одного среза дважды. Заимствование различных частей среза в принципе нормально, потому что они не перекрываются, но Rust недостаточно умён, чтобы это понять. Когда мы знаем, что код верный, но Rust этого не понимает, значит пришло время прибегнуть к небезопасному коду. Листинг 19-6 демонстрирует, как можно использовать unsafe блок, сырой указатель и вызовы небезопасных функций чтобы split_at_mut заработала: |