Язык программирования Rust
Скачать 7.02 Mb.
|
Кодирование состояний и поведения в виде типов Мы покажем вам, как переосмыслить шаблон "Состояние", чтобы получить другой набор компромиссов. Вместо того, чтобы полностью инкапсулировать состояния и переходы, так, чтобы внешний код не знал о них, мы будем кодировать состояния с помощью разных типов. Следовательно, система проверки типов Rust предотвратит попытки использовать черновые публикации, там где разрешены только опубликованные публикации, вызывая ошибки компиляции. Давайте рассмотрим первую часть main в листинге 17-11: Файл: src/main.rs Мы по-прежнему поддерживаем создание новых сообщений в состоянии "черновика" с помощью метода Post::new и возможность добавлять текст к содержимому публикации. Но вместо метода content у чернового сообщения, возвращающего пустую строку, мы сделаем так, что у черновых сообщений вообще не будет метода content . Таким fn main () { let mut post = Post::new(); post.add_text( "I ate a salad for lunch today" ); assert_eq! ( "" , post.content()); } образом, если мы попытаемся получить содержимое черновика, мы получим ошибку компилятора, сообщающую, что метод не существует. В результате мы не сможем случайно отобразить черновик содержимого записи в работающей программе, потому что этот код даже не скомпилируется. В листинге 17-19 показано определение структур Post и DraftPost , а также методов для каждой из них: Файл: src/lib.rs Листинг 17-19: Структура Post с методом content и структура DraftPost без метода content Обе структуры, Post и DraftPost , имеют приватное поле content , в котором хранится текст сообщения блога. Структуры больше не содержат поле state , потому что мы перемещаем кодирование состояния в типы структур. Структура Post будет представлять опубликованную публикацию, и у неё есть метод content , который возвращает content У нас все ещё есть функция Post::new , но вместо возврата экземпляра Post она возвращает экземпляр DraftPost . Поскольку поле content является приватным и нет никаких функций, которые возвращают Post , просто так создать экземпляр Post уже невозможно. Структура DraftPost имеет метод add_text , поэтому мы можем добавлять текст к content как и раньше, но учтите, что в DraftPost не определён метод content ! Так что теперь программа гарантирует, что все записи начинаются как черновики, а черновики pub struct Post { content: String , } pub struct DraftPost { content: String , } impl Post { pub fn new () -> DraftPost { DraftPost { content: String ::new(), } } pub fn content (& self ) -> & str { & self .content } } impl DraftPost { pub fn add_text (& mut self , text: & str ) { self .content.push_str(text); } } публикаций не имеют своего контента для отображения. Любая попытка обойти эти ограничения приведёт к ошибке компилятора. Реализация переходов в виде преобразований в другие типы Так как же получить опубликованный пост? Мы хотим обеспечить соблюдение правила, согласно которому черновик записи должен быть рассмотрен и утверждён до того, как он будет опубликован. Запись, находящаяся в состоянии проверки, по-прежнему не должна отображать содержимое. Давайте реализуем эти ограничения, добавив ещё одну структуру, PendingReviewPost , определив метод request_review у DraftPost , возвращающий PendingReviewPost , и определив метод approve у PendingReviewPost , возвращающий Post , как показано в листинге 17-20: Файл: src/lib.rs Листинг 17-20: Тип PendingReviewPost , который создаётся путём вызова request_review экземпляра DraftPost и метод approve , который превращает PendingReviewPost в опубликованный Post . Методы request_review и approve забирают во владение self , таким образом поглощая экземпляры DraftPost и PendingReviewPost , которые потом преобразуются в PendingReviewPost и опубликованную Post , соответственно. Таким образом, у нас не будет никаких долгоживущих экземпляров DraftPost , после того, как мы вызвали у них request_review и так далее. В структуре PendingReviewPost не определён метод content , поэтому попытка прочитать его содержимое приводит к ошибке компилятора, также как и в случае с DraftPost . Так как единственным способом получить опубликованный экземпляр Post , у которого действительно есть объявленный метод content , является вызов метода approve у экземпляра PendingReviewPost , а единственный способ получить PendingReviewPost - это вызвать метод request_review у impl DraftPost { // --snip-- pub fn request_review ( self ) -> PendingReviewPost { PendingReviewPost { content: self .content, } } } pub struct PendingReviewPost { content: String , } impl PendingReviewPost { pub fn approve ( self ) -> Post { Post { content: self .content, } } } экземпляра DraftPost , теперь мы закодировали процесс смены состояний записи блога с помощью системы типов. Но мы также должны внести небольшие изменения в main . Методы request_review и approve возвращают новые экземпляры, а не изменяют структуру, к которой они обращаются, поэтому нам нужно добавить больше выражений let post = , затеняя присваивания для сохранения возвращаемых экземпляров. Мы также не можем использовать утверждения (assertions), что для черновика и записи, ожидающей проверки, содержимое должно быть пустой строкой, они нам больше не нужны: теперь мы не сможем скомпилировать код, который пытается использовать содержимое записей в этих состояниях. Обновлённый код main показан в листинге 17-21: Файл: src/main.rs Листинг 17-21: Изменения в main , использующие новую реализацию процесса подготовки записи блога Изменения, которые нам нужно было внести в main , чтобы переназначить post означают, что эта реализация теперь не совсем соответствует объектно- ориентированному шаблону "Состояние": преобразования между состояниями больше не инкапсулированы внутри реализации Post полностью. Тем не менее, мы получили большую выгоду в том, что недопустимые состояния теперь невозможны из-за системы типов и проверки типов, которая происходит во время компиляции! У нас есть гарантия, что некоторые ошибки, такие как отображение содержимого неопубликованной публикации, будут обнаружены до того, как они дойдут до пользователей. Попробуйте выполнить задачи, предложенные в начале этого раздела, в версии крейта blog , каким он стал после листинга 17-20, чтобы сформировать своё мнение о дизайне этой версии кода. Обратите внимание, что некоторые задачи в этом варианте могут быть уже выполнены. Мы увидели, что хотя Rust и способен реализовывать объектно-ориентированные шаблоны проектирования, в нём также доступны и другие шаблоны, такие как кодирование состояния с помощью системы типов. Эти модели имеют различные компромиссы. Хотя вы, возможно, очень хорошо знакомы с объектно- ориентированными шаблонами, переосмысление проблем для использования use blog::Post; fn main () { let mut post = Post::new(); post.add_text( "I ate a salad for lunch today" ); let post = post.request_review(); let post = post.approve(); assert_eq! ( "I ate a salad for lunch today" , post.content()); } преимуществ и возможностей Rust может дать такие выгоды, как предотвращение некоторых ошибок во время компиляции. Объектно-ориентированные шаблоны не всегда будут лучшим решением в Rust из-за наличия определённых возможностей, таких как владение, которого нет у объектно-ориентированных языков. Итоги Независимо от того, что вы думаете о принадлежности Rust к объектно- ориентированным языкам после прочтения этой главы, теперь вы знаете, что можете использовать типаж-объекты, чтобы реализовать некоторые объектно- ориентированные свойства в Rust. Динамическая диспетчеризация может дать вашему коду некоторую гибкость в обмен на небольшое ухудшение производительности во время выполнения. Вы можете использовать эту гибкость для реализации объектно- ориентированных шаблонов, которые могут улучшить сопровождаемость вашего кода. В Rust также есть другие особенности, такие как владение, которых нет у объектно- ориентированных языков. Объектно-ориентированный шаблон не всегда будет лучшим способом использовать преимущества Rust, но является доступной опцией. Далее мы рассмотрим шаблоны, которые являются ещё одной возможностью языка Rust, дающей больше гибкости. Мы кратко встречались с ними на протяжении всей книги, но ещё не рассмотрели все их способности. Приступим! Шаблоны и сопоставление Шаблоны - это специальный синтаксис в Rust для сопоставления со структурой типов, как сложных, так и простых. Использование шаблонов в сочетании с выражениями match и другими конструкциями даёт вам больший контроль над потоком управления программы. Шаблон состоит из некоторой комбинации следующего: Литералы Деструктурированные массивы, перечисления, структуры или кортежи Переменные Специальные символы Заполнители Некоторые примеры шаблонов включают x , (a, 3) и Some(Color::Red) . В контекстах, в которых допустимы шаблоны, эти компоненты описывают форму данных. Затем наша программа сопоставляет значения с шаблонами, чтобы определить, имеет ли значение правильную форму данных для продолжения выполнения определённого фрагмента кода. Чтобы использовать шаблон, мы сравниваем его с некоторым значением. Если шаблон соответствует значению, мы используем части значения в нашем дальнейшем коде. Вспомните выражения match главы 6, в которых использовались шаблоны, например, описание машины для сортировки монет. Если значение в памяти соответствует форме шаблона, мы можем использовать именованные части шаблона. Если этого не произойдёт, то не выполнится код, связанный с шаблоном. Эта глава - справочник по всем моментам, связанным с шаблонами. Мы расскажем о допустимых местах использования шаблонов, разнице между опровержимыми и неопровержимыми шаблонами и про различные виды синтаксиса шаблонов, которые вы можете увидеть. К концу главы вы узнаете, как использовать шаблоны для ясного выражения многих понятий. Все случаи, где могут быть использованы шаблоны В процессе использования языка Rust вы часто используете шаблоны, даже не осознавая этого! В этом разделе обсуждаются все случаи, где использование шаблонов является корректным. Ветки match Как обсуждалось в главе 6, мы используем шаблоны в ветках выражений match Формально выражения match определяется как ключевое слово match , значение используемое для сопоставления, одна или несколько веток, которые состоят из шаблона и выражения для выполнения, если значение соответствует шаблону этой ветки, как здесь: Например, вот выражение match из листинга 6-5, которое соответствует значению Option в переменной x : Шаблонами в этом выражении match являются None и Some(i) слева от каждой стрелки. Одно из требований к выражениям match состоит в том, что они должны быть исчерпывающими (exhaustive) в том смысле, что они должны учитывать все возможности для значения в выражении match . Один из способов убедиться, что вы рассмотрели каждую возможность - это иметь шаблон перехвата всех вариантов в последней ветке выражения: например, имя переменной, совпадающее с любым значением, никогда не может потерпеть неудачу и таким образом, охватывает каждый оставшийся случай. Специальный шаблон _ будет соответствовать чему угодно, но он никогда не привязывается к переменной, поэтому он часто используется в последней ветке. Шаблон _ может быть полезен, если вы, например, хотите игнорировать любое не указанное значение. Мы рассмотрим шаблон _ более подробно в разделе "Игнорирование значений в шаблоне позже в этой главе. Условные выражения if let match VALUE { PATTERN => EXPRESSION, PATTERN => EXPRESSION, PATTERN => EXPRESSION, } match x { None => None , Some (i) => Some (i + 1 ), } В главе 6 мы обсуждали, как использовать выражения if let как правило в качестве более короткого способа записи эквивалента match , которое обрабатывает только один случай. Дополнительно if let может иметь соответствующий else , содержащий код для выполнения, если шаблон выражения if let не совпадает. В листинге 18-1 показано, что можно также смешивать и сопоставлять выражения if let , else if и else if let . Это даёт больше гибкости, чем match выражение, в котором можно выразить только одно значение для сравнения с шаблонами. Кроме того, условия в серии if let , else if , else if let не обязаны соотноситься друг с другом. Код в листинге 18-1 показывает последовательность проверок нескольких условий, определяющих каким должен быть цвет фона. В данном примере мы создали переменные с предопределёнными значениями, которые в реальной программе могли бы быть получены из пользовательского ввода. Файл: src/main.rs Листинг 18-1: Использование условных конструкций if let , else if , else if let , и else Если пользователь указывает любимый цвет, то этот цвет используется в качестве цвета фона. Если любимый цвет не указан, и сегодня вторник, то цвет фона - зелёный. Иначе, если пользователь указывает свой возраст в виде строки, и мы можем успешно проанализировать её и представить в виде числа, то цвет будет либо фиолетовым, либо оранжевым, в зависимости от значения числа. Если ни одно из этих условий не выполняется, то цвет фона будет синим. Эта условная структура позволяет поддерживать сложные требования. С жёстко закодированными значениями, которые у нас здесь есть, этот пример напечатает Using purple as the background color fn main () { let favorite_color: Option <& str > = None ; let is_tuesday = false ; let age: Result < u8 , _> = "34" .parse(); if let Some (color) = favorite_color { println! ( "Using your favorite color, {color}, as the background" ); } else if is_tuesday { println! ( "Tuesday is green day!" ); } else if let Ok (age) = age { if age > 30 { println! ( "Using purple as the background color" ); } else { println! ( "Using orange as the background color" ); } } else { println! ( "Using blue as the background color" ); } } Можно увидеть, что if let может также вводить затенённые переменные, как это можно сделать в match ветках: строка if let Ok(age) = age вводит новую затенённую переменную age , которая содержит значение внутри варианта Ok . Это означает, что нам нужно поместить условие if age > 30 внутри этого блок: мы не можем объединить эти два условия в if let Ok(age) = age && age > 30 . Затенённый age , который мы хотим сравнить с 30, не является действительным, пока не начнётся новая область видимости с фигурной скобки. Недостатком использования if let выражений является то, что компилятор не проверяет полноту (exhaustiveness) всех вариантов, в то время как с помощью выражения match это происходит. Если мы не напишем последний блок else и, благодаря этому, пропустим обработку некоторых случаев, компилятор не предупредит нас о возможной логической ошибке. Условные циклы while let Аналогично конструкции if let , конструкция условного цикла while let позволяет повторять цикл while до тех пор, пока шаблон продолжает совпадать. Пример в листинге 18-2 демонстрирует цикл while let , который использует вектор в качестве стека и печатает значения вектора в порядке, обратном тому, в котором они были помещены. Листинг 18-2: Использование цикла while let для печати значений до тех пор, пока stack.pop() возвращает Some В этом примере выводится 3, 2, а затем 1. Метод pop извлекает последний элемент из вектора и возвращает Some(value) . Если вектор пуст, то pop возвращает None . Цикл while продолжает выполнение кода в своём блоке, пока pop возвращает Some . Когда pop возвращает None , цикл останавливается. Мы можем использовать while let для удаления каждого элемента из стека. Цикл for В цикле for значение, которое следует непосредственно за ключевым словом for , является шаблоном. Например, в for x in y выражение x является шаблоном. В let mut stack = Vec ::new(); stack.push( 1 ); stack.push( 2 ); stack.push( 3 ); while let Some (top) = stack.pop() { println! ( "{}" , top); } листинге 18-3 показано, как использовать шаблон в цикле for , чтобы деструктурировать или разбить кортеж как часть цикла for Листинг 18-3: Использование шаблона в цикле for для деструктурирования кортежа Код в листинге 18-3 выведет следующее: Мы адаптируем итератор с помощью метода enumerate , чтобы он генерировал кортеж, состоящий из значения и индекса этого значения. Первым сгенерированным значением будет кортеж (0, 'a') . Когда это значение сопоставляется с шаблоном (index, value) , index будет равен 0 , а value будет равно 'a' и будет напечатана первая строка выходных данных. Оператор let До этой главы мы подробно обсуждали только использование шаблонов с match и if let , но на самом деле, мы использовали шаблоны и в других местах, в том числе в операторах let . Например, рассмотрим следующее простое назначение переменной с помощью let : Каждый раз, когда вы использовали подобным образом оператор let , вы использовали шаблоны, хотя могли и не осознавать этого! Более формально оператор let выглядит так: В выражениях типа let x = 5; с именем переменной в слоте PATTERN , имя переменной является просто отдельной, простой формой шаблона. Rust сравнивает выражение с шаблоном и присваивает любые имена, которые он находит. Так что в примере let x = 5; , x - это шаблон, который означает "привязать то, что соответствует здесь, let v = vec! [ 'a' , 'b' , 'c' ]; for (index, value) in v.iter().enumerate() { println! ( "{} is at index {}" , value, index); } $ cargo run Compiling patterns v0.1.0 (file:///projects/patterns) Finished dev [unoptimized + debuginfo] target(s) in 0.52s Running `target/debug/patterns` a is at index 0 b is at index 1 c is at index 2 let x = 5 ; let PATTERN = EXPRESSION; переменной x ". Поскольку имя x является полностью шаблоном, этот шаблон фактически означает "привязать все к переменной x независимо от значения". Чтобы более чётко увидеть аспект сопоставления с шаблоном let , рассмотрим листинг 18-4, в котором используется шаблон с let для деструктурирования кортежа. |