ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1151
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Если нужно объединить несколько строк, поведение оператора
+
становится громоздким:
Здесь переменная s
будет содержать tic-tac-toe
. С множеством символов
+
и "
становится трудно понять, что происходит. Для более сложного комбинирования строк можно использовать макрос format!
:
Этот код также устанавливает переменную s
в значение tic-tac-toe
. Макрос format!
работает тем же способом что макрос println!
, , но вместо вывода на экран возвращает тип
String с содержимым. Версия кода с использованием format!
значительно легче читается, а также код, сгенерированный макросом format!
, использует ссылки, а значит не забирает во владение ни один из его параметров.
Индексирование в строках
Доступ к отдельным символам в строке, при помощи ссылки на них по индексу, является допустимой и распространённой операцией во многих других языках программирования. Тем не менее, если вы попытаетесь получить доступ к частям
String
, используя синтаксис индексации в Rust, то вы получите ошибку. Рассмотрим неверный код в листинге 8-19.
Листинг 8-19: Попытка использовать синтаксис индекса со строкой
Этот код приведёт к следующей ошибке:
let s1 =
String
::from(
"tic"
); let s2 =
String
::from(
"tac"
); let s3 =
String
::from(
"toe"
); let s = s1 +
"-"
+ &s2 +
"-"
+ &s3; let s1 =
String
::from(
"tic"
); let s2 =
String
::from(
"tac"
); let s3 =
String
::from(
"toe"
); let s = format!
(
"{}-{}-{}"
, s1, s2, s3); let s1 =
String
::from(
"hello"
); let h = s1[
0
];
Ошибка и примечание говорит, что в Rust строки не поддерживают индексацию. Но почему так? Чтобы ответить на этот вопрос, нужно обсудить то, как Rust хранит строки в памяти.
Внутреннее представление
Тип
String является оболочкой над типом
Vec
. Давайте посмотрим на несколько закодированных корректным образом в UTF-8 строк из примера листинга 8-14. Начнём с этой:
В этом случае len будет 4, что означает вектор, хранит строку "Hola" длиной 4 байта.
Каждая из этих букв занимает 1 байт при кодировании в UTF-8. Но как насчёт следующей строки? (Обратите внимание, что эта строка начинается с заглавной кириллической "З", а не арабской цифры 3.)
Отвечая на вопрос, какова длина строки, вы можете ответить 12. Однако ответ Rust - 24,
что равно числу байт, необходимых для кодирования «Здравствуйте» в UTF-8, так происходит, потому что каждое скалярное значение Unicode символа в этой строке занимает 2 байта памяти. Следовательно, индекс по байтам строки не всегда бы соответствовал действительному скалярному Unicode значению. Для демонстрации рассмотрим этот недопустимый код Rust:
$
cargo run
Compiling collections v0.1.0 (file:///projects/collections) error[E0277]: the type `String` cannot be indexed by `{integer}`
-->
src/main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
= help: the following other types implement trait `Index
For more information about this error, try `rustc --explain E0277`. error: could not compile `collections` due to previous error let hello =
String
::from(
"Hola"
); let hello =
String
::from(
"Здравствуйте"
); let hello =
"Здравствуйте"
; let answer = &hello[
0
];
Каким должно быть значение переменной 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
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!
Хранение ключей со связанными значениями в
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