Материал: Язык программирования Rust

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

Каким должно быть значение переменной answer
? Должно ли оно быть значением первой буквы
З
? При кодировке в UTF-8, первый байт значения
З
равен
208
, а второй -
151
, поэтому значение в answer на самом деле должно быть
208
, но само по себе
208
не является действительным символом. Возвращение
208
, скорее всего не то, что хотел бы получить пользователь: ведь он ожидает первую букву этой строки; тем не менее, это единственный байт данных, который в Rust доступен по индексу 0. Пользователи обычно не хотят получить значение байта, даже если строка содержит только латинские буквы:
если
&"hello"[0] было бы допустимым кодом, который вернул значение байта, то он вернул бы
104
, а не h
Таким образом, чтобы предотвратить возврат непредвиденного значения, вызывающего ошибки которые не могут быть сразу обнаружены, Rust просто не компилирует такой код и предотвращает недопонимание на ранних этапах процесса разработки.
Байты, скалярные значения и кластеры графем! Боже мой!
Ещё один момент, касающийся UTF-8, заключается в том, что на самом деле существует три способа рассмотрения строк с точки зрения Rust: как байты, как скалярные значения и как кластеры графем (самая близкая вещь к тому, что мы назвали бы буквами).
Если посмотреть на слово языка хинди «नम े», написанное в транскрипции Devanagari, то оно хранится как вектор значений u8
который выглядит следующим образом:
Эти 18 байт являются именно тем, как компьютеры в конечном итоге сохранят в памяти эту строку. Если мы посмотрим на 18 байт как на скалярные Unicode значения, которые являются Rust типом char
, то байты будут выглядеть так:
Здесь есть шесть значений типа char
, но четвёртый и шестой являются не буквами: они диакритики, специальные обозначения которые не имеют смысла сами по себе.
Наконец, если мы посмотрим на байты как на кластеры графем, то получим то, что человек назвал бы словом на хинди состоящем из четырёх букв:
Rust предоставляет различные способы интерпретации необработанных строковых данных, которые компьютеры хранят так, чтобы каждой программе можно было выбрать необходимую интерпретацию, независимо от того, на каком человеческом языке представлены эти данные.
Последняя причина, по которой Rust не позволяет нам индексировать
String для получения символов является то, что программисты ожидают, что операции
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
['न', 'म', 'स', '◌्', 'त', '◌े']
["न", "म", "स्", "ते"]
индексирования всегда имеют постоянное время (O(1)) выполнения. Но невозможно гарантировать такую производительность для
String
, потому что Rust понадобилось бы пройтись по содержимому от начала до индекса, чтобы определить, сколько было действительных символов.
Срезы строк
Индексирование строк часто является плохой идеей, потому что не ясно каким должен быть возвращаемый тип такой операции: байтовым значением, символом, кластером графем или срезом строки. Поэтому Rust просит вас быть более конкретным, если действительно требуется использовать индексы для создания срезов строк.
Вместо индексации с помощью числового индекса
[]
, вы можете использовать оператор диапазона
[]
при создании среза строки в котором содержится указание на то,
срез каких байтов надо делать:
Здесь переменная s
будет типа
&str который содержит первые 4 байта строки. Ранее мы упоминали, что каждый из этих символов был по 2 байта, что означает, что s
будет содержать
Зд
Что бы произошло, если бы мы использовали
&hello[0..1]
? Ответ: Rust бы запаниковал во время выполнения точно так же, как если бы обращались к недействительному индексу в векторе:
Вы должны использовать диапазоны для создания срезов строк с осторожностью, потому что это может привести к сбою вашей программы.
Методы для перебора строк
Лучший способ работать с фрагментами строк - чётко указать, нужны ли вам символы или байты. Для отдельных скалярных значений в Юникоде используйте метод chars
Вызов chars у “Зд” выделяет и возвращает два значения типа char
, и вы можете выполнить итерацию по результату для доступа к каждому элементу:
let hello =
"Здравствуйте"
; let s = &hello[
0 4
];
$
cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections` thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З'
(bytes 0..2) of `Здравствуйте`', library/core/src/str/mod.rs:127:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Код напечатает следующее:
Метод bytes возвращает каждый байт, который может быть подходящим в другой предметной области:
Этот код выведет четыре байта, составляющих эту строку:
Но делая так, обязательно помните, что валидные скалярные Unicode значения могут состоять более чем из одного байта.
Извлечение кластеров графем из строк, как в случае с языком хинди, является сложным,
поэтому эта функциональность не предусмотрена стандартной библиотекой. На crates.io есть доступные библиотеки, если Вам нужен данный функционал.
Строки не так просты
Подводя итог, становится ясно, что строки сложны. Различные языки программирования реализуют различные варианты того, как представить эту сложность для программиста.
В Rust решили сделать правильную обработку данных
String поведением по умолчанию для всех программ Rust, что означает, что программисты должны заранее продумать обработку UTF-8 данных. Этот компромисс раскрывает большую сложность строк, чем в других языках программирования, но это предотвращает от необходимости обрабатывать ошибки, связанные с не-ASCII символами которые могут появиться в ходе разработки позже.
Хорошая новость состоит в том что стандартная библиотека предлагает множество функциональных возможностей, построенных на основе типов
String и
&str
, чтобы помочь правильно обрабатывать эти сложные ситуации. Обязательно ознакомьтесь с for c in
"Зд"
.chars() { println!
(
"{}"
, c);
}
З д for b in
"Зд"
.bytes() { println!
(
"{}"
, b);
}
208 151 208 180
документацией для полезных методов, таких как contains для поиска в строке и replace для замены частей строки другой строкой.
Давайте переключимся на что-то немного менее сложное: HashMap!

Хранение ключей со связанными значениями в
HashMap
Последняя коллекция, которую мы рассмотрим, будет hash map (хеш-карта). Тип
HashMap
хранит ключи типа
K
на значения типа
V
. Данная структура организует и хранит данные с помощью функции хеширования. Во множестве языков программирования реализована данная структура, но часто с разными наименованиями: такими как hash, map, object, hash table, dictionary или ассоциативный массив.
Хеш-карты полезны, когда нужно искать данные не используя индекс, как это например делается в векторах, а с помощью ключа, который может быть любого типа. Например, в игре вы можете отслеживать счёт каждой команды в хеш-карте, в которой каждый ключ - это название команды, а значение - счёт команды. Имея имя команды, вы можете получить её счёт из хеш-карты.
В этом разделе мы рассмотрим базовый API хеш-карт. Остальной набор полезных функций скрывается в объявлении типа
HashMap
. Как и прежде, советуем обратиться к документации по стандартной библиотеке для получения дополнительной информации.
Создание новой хеш-карты
Создать пустую хеш-карту можно с помощью new
, а добавить в неё элементы - с помощью insert
. В листинге 8-20 мы отслеживаем счёт двух команд, синей Blue и жёлтой Yellow. Синяя команда набрала 10 очков, а жёлтая команда - 50.
Листинг 8-20: Создание новой хеш-карты и вставка в неё пары ключей и значений
Обратите внимание, что нужно сначала указать строку use std::collections::HashMap;
для её подключения из коллекций стандартной библиотеки. Из трёх коллекций данная является наименее используемой, поэтому она не подключается в область видимости функцией автоматического импорта (prelude). Хеш-карты также имеют меньшую поддержку со стороны стандартной библиотеки; например, нет встроенного макроса для их конструирования.
Подобно векторам, хеш-карты хранят свои данные в куче. Здесь тип
HashMap имеет в качестве типа ключей
String
, а в качестве типа значений тип i32
. Как и векторы,
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Yellow"
),
50
);

HashMap однородны: все ключи должны иметь одинаковый тип и все значения должны иметь тоже одинаковый тип.
Доступ к данным в HashMap
Мы можем получить значение из HashMap по ключу, с помощью метода get
, как показано в листинге 8-21.
Листинг 8-21: Доступ к очкам команды "Blue", которые хранятся в хеш-карте
Здесь score будет иметь количество очков, связанное с командой "Blue", результат будет
10
. Метод get возвращает
Option<&V>
; если для какого-то ключа нет значения в
HashMap, get вернёт
None
. Из-за такого подхода программе следует обрабатывать
Option
, вызывая copied для получения
Option
вместо
Option<&i32>
, затем unwrap_or для установки score в ноль, если scores не содержит данных по этому ключу.
Мы можем перебирать каждую пару ключ/значение в HashMap таким же образом, как мы делали с векторами, используя цикл for
:
Этот код будет печатать каждую пару в произвольном порядке:
Хеш-карты и владение
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Yellow"
),
50
); let team_name =
String
::from(
"Blue"
); let score = scores.get(&team_name).copied().unwrap_or(
0
); use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Yellow"
),
50
); for
(key, value) in
&scores { println!
(
"{}: {}"
, key, value);
}
Yellow: 50
Blue: 10

Для типов, которые реализуют типаж
Copy
, например i32
, значения копируются в
HashMap. Для значений со владением, таких как
String
, значения будут перемещены в хеш-карту и она станет владельцем этих значений, как показано в листинге 8-22.
1   ...   14   15   16   17   18   19   20   21   ...   62
Листинг 8-22: Показывает, что ключи и значения находятся во владении HashMap, как только они быливставленыМы не можем использовать переменные field_name и field_value после того, как их значения были перемещены в HashMap вызовом метода insertЕсли мы вставим в HashMap ссылки на значения, то они не будут перемещены вHashMap. Значения, на которые указывают ссылки, должны быть действительными хотя бы до тех пор, пока хеш-карта действительна. Мы поговорим подробнее об этих вопросах в разделе "Валидация ссылок при помощи времён жизни" главы 10.Обновление данных в HashMapХотя количество ключей и значений может увеличиваться в HashMap, каждый ключ может иметь только одно значение, связанное с ним в один момент времени (обратное утверждение неверно: команды "Blue" и "Yellow" могут хранить в хеш-карте scores одинаковое количество очков, например 10).Когда вы хотите изменить данные в хеш-карте, необходимо решить, как обрабатывать случай, когда ключ уже имеет назначенное значение. Можно заменить старое значение новым, полностью игнорируя старое. Можно сохранить старое значение и игнорировать новое, или добавлять новое значение, если только ключ ещё не имел значения. Или можно было бы объединить старое значение и новое значение. Давайте посмотрим, как сделать каждый из вариантов!Перезапись старых значенийЕсли мы вставим ключ и значение в HashMap, а затем вставим тот же ключ с новым значением, то старое значение связанное с этим ключом, будет заменено на новое. Даже несмотря на то, что код в листинге 8-23 вызывает insert дважды, хеш-карта будет содержать только одну пару ключ/значение, потому что мы вставляем значения для одного и того же ключа - ключа команды "Blue".use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and// see what compiler error you get!

Листинг 8-23: Замена значения, хранимого в конкретном ключе
Код напечатает
{"Blue": 25}
. Начальное значение
10
было перезаписано.
Вставка значения только в том случае, когда ключ не имеет значения
Обычно проверяют, существует ли конкретный ключ в хеш-карте со значением, а затем предпринимаются следующие действия: если ключ существует в хеш-карте,
существующее значение должно оставаться таким, какое оно есть. Если ключ не существует, то вставляют его и значение для него.
Хеш-карты имеют для этого специальный API, называемый entry
, который принимает ключ для проверки в качестве входного параметра. Возвращаемое значение метода entry
- это перечисление
Entry
, с двумя вариантами: первый представляет значение,
которое может существовать, а второй говорит о том, что значение отсутствует.
Допустим, мы хотим проверить, имеется ли ключ и связанное с ним значение для команды "Yellow". Если хеш-карта не имеет значения для такого ключа, то мы хотим вставить значение 50. То же самое мы хотим проделать и для команды "Blue".
Используем API entry в коде листинга 8-24.
Листинг 8-24: Использование метода
entry
для вставки значения только в том случае, когда ключ не имеет
значения
Метод or_insert определён в
Entry так, чтобы возвращать изменяемую ссылку на соответствующее значение ключа внутри варианта перечисления
Entry
, когда этот ключ существует, а если его нет, то вставлять параметр в качестве нового значения этого ключа и возвращать изменяемую ссылку на новое значение. Эта техника намного чище,
чем самостоятельное написание логики и, кроме того, она более безопасна и согласуется с правилами заимствования.
use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.insert(
String
::from(
"Blue"
),
25
); println!
(
"{:?}"
, scores); use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
String
::from(
"Blue"
),
10
); scores.entry(
String
::from(
"Yellow"
)).or_insert(
50
); scores.entry(
String
::from(
"Blue"
)).or_insert(
50
); println!
(
"{:?}"
, scores);
Источник: https://files.student-it.ru/previewfile/264058