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

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

Если вы используете оба стиля для одного и того же модуля, вы получите ошибку компилятора. Использование сочетания обоих стилей для разных модулей в одном проекте разрешено, но это может сбивать с толку людей, перемещающихся по вашему проекту.
Основным недостатком стиля, в котором используются файлы с именами mod.rs,
является то, что в вашем проекте может оказаться много файлов с именами mod.rs,
что может привести к путанице, если вы одновременно откроете их в редакторе.
Мы перенесли код каждого модуля в отдельный файл, а дерево модулей осталось прежним. Вызовы функций в eat_at_restaurant будут работать без каких-либо изменений, несмотря на то, что определения находятся в разных файлах. Этот метод позволяет перемещать модули в новые файлы по мере увеличения их размеров.
Обратите внимание, что оператор pub use crate::front_of_house::hosting в src/lib.rs
также не изменился, и use не влияет на то, какие файлы компилируются как часть крейта. Ключевое слово mod объявляет модули, и Rust ищет в файле с тем же именем,
что и у модуля, код, который входит в этот модуль.
Итог
Rust позволяет разбить пакет на несколько крейтов и крейт - на модули, так что вы можете ссылаться на элементы, определённые в одном модуле, из другого модуля. Это можно делать при помощи указания абсолютных или относительных путей. Эти пути можно добавить в область видимости оператором use
, поэтому вы можете пользоваться более короткими путями для многократного использования элементов в этой области видимости. Код модуля по умолчанию является приватным, но можно сделать определения общедоступными, добавив ключевое слово pub
В следующей главе мы рассмотрим некоторые коллекции структур данных из стандартной библиотеки, которые вы можете использовать в своём аккуратно организованном коде.

Коллекции стандартной библиотеки
Стандартная библиотека содержит несколько полезных структур данных, которые называются коллекциями. Большая часть других типов данных представляют собой хранение конкретного значения, но особенностью коллекций является хранение множества однотипных значений. В отличии от массива или кортежа данные коллекций хранятся в куче, а это значит, что размер коллекции может быть неизвестен в момент компиляции программы. Он может изменяться (увеличиваться, уменьшаться) во время работы программы. Каждый вид коллекций имеет свои возможности и отличается по производительности, так что выбор конкретной коллекции зависит от ситуации и является умением разработчика, вырабатываемым со временем. В этой главе будет рассмотрено несколько коллекций:
Вектор (vector) - позволяет нам сохранять различное количество последовательно хранящихся значений,
Строка (string) - это последовательность символов. Мы же упоминали тип
String ранее, но в данной главе мы поговорим о нем подробнее.
Хеш таблица (hash map) - коллекция которая позволяет хранить перечень ассоциаций значения с ключом (перечень пар ключ:значение). Является конкретной реализацией более общей структуры данных называемой map.
Для того, чтобы узнать о других видах коллекций предоставляемых стандартной библиотекой смотрите документацию
Мы обсудим как создавать и обновлять вектора, строки и хеш таблицы, а также объясним что делает каждую из них особенной.

Хранение списков значений в векторах
Первым типом коллекции, который мы разберём, будет
Vec
, также известный как
вектор (vector). Векторы позволяют хранить более одного значения в одной структуре данных, хранящей элементы в памяти один за другим. Векторы могут хранить данные только одного типа. Их удобно использовать, когда нужно хранить список элементов,
например, список текстовых строк из файле, или список цен товаров в корзине покупок.
1   ...   12   13   14   15   16   17   18   19   ...   62
Создание нового вектораЧтобы создать новый пустой вектор, мы вызываем функцию Vec::new, как показано в листинге 8-1.Листинг 8-1: Создание нового пустого вектора для хранения значений типа i32Обратите внимание, что здесь мы добавили аннотацию типа. Поскольку мы не вставляем никаких значений в этот вектор, Rust не знает, какие элементы мы собираемся хранить.Это важный момент. Векторы реализованы с использованием обобщённых типов; мы рассмотрим, как использовать обобщённые типы с вашими собственными типами вГлаве 10. А пока знайте, что тип Vec
, предоставляемый стандартной библиотекой,
может хранить любой тип. Когда мы создаём новый вектор для хранения конкретного типа, мы можем указать этот тип в угловых скобках. В листинге 8-1 мы сообщили Rust, что
Vec
в v
будет хранить элементы типа i32
Чаще всего вы будете создавать
Vec
с начальными значениями и Rust может определить тип сохраняемых вами значений, но иногда вам всё же придётся указывать аннотацию типа. Для удобства Rust предоставляет макрос vec!
, который создаст новый вектор, содержащий заданные вами значения. В листинге 8-2 создаётся новый
Vec
,
который будет хранить значения
1
,
2
и
3
. Целочисленный тип — i32, потому что это целочисленный тип по умолчанию, как мы обсуждали в разделе «Типы данных» главы 3.
Числовым типом является i32
, потому что это тип по умолчанию для целочисленных значений, о чём упоминалось в разделе
“Типы данных”
Главы 3.
Листинг 8-2: Создание нового вектора, содержащего значения
Поскольку мы указали начальные значения типа i32
, Rust может сделать вывод, что тип переменной v
это
Vec
и аннотация типа здесь не нужна. Далее мы посмотрим как изменять вектор.
let v:
Vec
<
i32
> =
Vec
::new(); let v = vec!
[
1
,
2
,
3
];

Изменение вектора
Чтобы создать вектор и затем добавить к нему элементы, можно использовать метод push показанный в листинге 8-3.
Листинг 8-3: Использование метода
push
для добавления значений в вектор
Как и с любой переменной, если мы хотим изменить её значение, нам нужно сделать её
изменяемой с помощью ключевого слова mut
, что обсуждалось в Главе 3. Все числа которые мы помещаем в вектор имеют тип i32
по этому Rust с лёгкостью выводит тип вектора, по этой причине нам не нужна здесь аннотация типа вектора
Vec
Чтение данных вектора
Есть два способа сослаться на значение, хранящееся в векторе: с помощью индекса или метода get
. В следующих примерах для большей ясности мы указали типы значений,
возвращаемых этими функциями.
В листинге 8-4 показаны оба метода доступа к значению в векторе: либо с помощью синтаксиса индексации и с помощью метода get
Листинг 8-4. Использование синтаксиса индексации и метода
get
для доступа к элементу в векторе
Обратите внимание здесь на пару деталей. Мы используем значение индекса
2
для получения третьего элемента: векторы индексируются начиная с нуля. Указывая
&
и
[]
мы получаем ссылку на элемент по указанному индексу. Когда мы используем метод get содержащего индекс, переданный в качестве аргумента, мы получаем тип
Option<&T>
,
который мы можем проверить с помощью match let mut v =
Vec
::new(); v.push(
5
); v.push(
6
); v.push(
7
); v.push(
8
); let v = vec!
[
1
,
2
,
3
,
4
,
5
]; let third: &
i32
= &v[
2
]; println!
(
"The third element is {}"
, third); let third:
Option
<&
i32
> = v.get(
2
); match third {
Some
(third) => println!
(
"The third element is {}"
, third),
None
=> println!
(
"There is no third element."
),
}

Причина, по которой Rust предоставляет два способа ссылки на элемент, заключается в том, что вы можете выбрать, как программа будет себя вести, когда вы попытаетесь использовать значение индекса за пределами диапазона существующих элементов. В
качестве примера давайте посмотрим, что происходит, когда у нас есть вектор из пяти элементов, а затем мы пытаемся получить доступ к элементу с индексом 100 с помощью каждого метода, как показано в листинге 8-5.
Листинг 8-5. Попытка доступа к элементу с индексом 100 в векторе, содержащем пять элементов
Когда мы запускаем этот код, первая строка с
&v[100]
вызовет панику программы,
потому что происходит попытка получить ссылку на несуществующий элемент. Такой подход лучше всего использовать, когда вы хотите, чтобы ваша программа аварийно завершила работу при попытке доступа к элементу за пределами вектора.
Когда методу get передаётся индекс, который находится за пределами вектора, он без паники возвращает
None
. Вы могли бы использовать такой подход, если доступ к элементу за пределами диапазона вектора происходит время от времени при нормальных обстоятельствах. Тогда ваш код будет иметь логику для обработки наличия
Some(&element)
или
None
, как обсуждалось в Главе 6. Например, индекс может исходить от человека, вводящего число. Если пользователь случайно введёт слишком большое число, то программа получит значение
None и у вас будет возможность сообщить пользователю, сколько элементов находится в текущем векторе, и дать ему возможность ввести допустимое значение. Такое поведение было бы более дружелюбным для пользователя, чем внезапный сбой программы из-за опечатки!
Когда у программы есть действительная ссылка, borrow checker (средство проверки заимствований), обеспечивает соблюдение правил владения и заимствования
(описанные в Главе 4), чтобы гарантировать, что эта ссылка и любые другие ссылки на содержимое вектора остаются действительными. Вспомните правило, которое гласит,
что у вас не может быть изменяемых и неизменяемых ссылок в одной и той же области.
Это правило применяется в листинге 8-6, где мы храним неизменяемую ссылку на первый элемент вектора и затем пытаемся добавить элемент в конец вектора. Данная программа не будет работать, если мы также попробуем сослаться на данный элемент позже в функции:
let v = vec!
[
1
,
2
,
3
,
4
,
5
]; let does_not_exist = &v[
100
]; let does_not_exist = v.get(
100
); let mut v = vec!
[
1
,
2
,
3
,
4
,
5
]; let first = &v[
0
]; v.push(
6
); println!
(
"The first element is: {}"
, first);

Листинг 8-6. Попытка добавить некоторый элемент в вектор, в то время когда есть ссылка на элемент
вектора
Компиляция этого кода приведёт к ошибке:
Код в листинге 8-6 может выглядеть так, как будто он должен работать. Почему ссылка на первый элемент должна заботиться об изменениях в конце вектора? Эта ошибка возникает из-за особенности того, как работают векторы: поскольку векторы размещают значения в памяти друг за другом, добавление нового элемента в конец вектора может потребовать выделения новой памяти и копирования старых элементов в новое пространство, если нет достаточного места, чтобы разместить все элементы друг за другом там, где в данный момент хранится вектор. В этом случае ссылка на первый элемент будет указывать на освобождённую память. Правила заимствования предотвращают попадание программ в такую ситуацию.
Примечание: Дополнительные сведения о реализации типа
Vec
смотрите в разделе "The Rustonomicon"
Перебор значений в векторе
Для доступа к каждому элементу вектора по очереди, мы итерируем все элементы,
вместо использования индексов для доступа к одному за раз. В листинге 8-7 показано,
как использовать цикл for для получения неизменяемых ссылок на каждый элемент в векторе значений типа i32
и их вывода.
$
cargo run
Compiling collections v0.1.0 (file:///projects/collections) error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
-->
src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`. error: could not compile `collections` due to previous error

Листинг 8-7. Печать каждого элемента векторе, при помощи итерирования по элементам вектора с
помощью цикла
for
Мы также можем итерировать изменяемые ссылки на каждый элемент изменяемого вектора, чтобы вносить изменения во все элементы. Цикл for в листинге 8-8 добавит
50
к каждому элементу.
Листинг 8-8. Итерирование и изменение элементов вектора по изменяемым ссылкам
Чтобы изменить значение на которое ссылается изменяемая ссылка, мы должны использовать оператор разыменования ссылки
*
для получения значения по ссылке в переменной i
прежде чем использовать оператор
+=
. Мы поговорим подробнее об операторе разыменования в разделе
“Следование по указателю к значению с помощью оператора разыменования”
Главы 15.
Перебор вектора, будь то неизменяемый или изменяемый, безопасен из-за правил проверки заимствования. Если бы мы попытались вставить или удалить элементы в телах цикла for в листингах 8-7 и 8-8, мы бы получили ошибку компилятора, подобную той, которую мы получили с кодом в листинге 8-6. Ссылка на вектор, содержащийся в цикле for, предотвращает одновременную модификацию всего вектора.
Использование перечислений для хранения множества разных типов
Векторы могут хранить значения только одинакового типа. Это может быть неудобно;
определённо могут быть случаи когда надо хранить список элементов разных типов. К
счастью, варианты перечисления определены для одного и того же типа перечисления,
поэтому, когда нам нужен один тип для представления элементов разных типов, мы можем определить и использовать перечисление!
Например, мы хотим получить значения из строки в электронной таблице где некоторые столбцы строки содержат целые числа, некоторые числа с плавающей точкой, а другие - строковые значения. Можно определить перечисление, варианты которого будут содержать разные типы значений и тогда все варианты перечисления будут считаться одними и тем же типом: типом самого перечисления. Затем мы можем создать вектор для хранения этого перечисления и, в конечном счёте, для хранения различных типов.
Мы покажем это в листинге 8-9.
let v = vec!
[
100
,
32
,
57
]; for i in
&v { println!
(
"{}"
, i);
} let mut v = vec!
[
100
,
32
,
57
]; for i in
&
mut v {
*i +=
50
;
}

Листинг 8-9: Определение
enum
для хранения значений разных типов в одном векторе
Rust должен знать, какие типы будут в векторе во время компиляции, чтобы точно знать сколько памяти в куче потребуется для хранения каждого элемента. Мы также должны чётко указать, какие типы разрешены в этом векторе. Если бы Rust позволял вектору содержать любой тип, то был бы шанс что один или несколько типов вызовут ошибки при выполнении операций над элементами вектора. Использование перечисления вместе с выражением match означает, что во время компиляции Rust гарантирует, что все возможные случаи будут обработаны, как обсуждалось в главе 6.
Если вы не знаете исчерпывающий набор типов, которые программа получит во время выполнения для хранения в векторе, то техника использования перечисления не сработает. Вместо этого вы можете использовать типаж-объект, который мы рассмотрим в главе 17.
Теперь, когда мы обсудили некоторые из наиболее распространённых способов использования векторов, обязательно ознакомьтесь с документацией по API вектора для всего множества полезных методов, определённых в
Vec
стандартной библиотеки.
Например, в дополнение к методу push
, существует метод pop
, который удаляет и возвращает последний элемент.
Удаление элементов из вектора
Подобно структурам struct
, вектор высвобождает свою память когда выходит из области видимости, что показано в листинге 8-10.
Листинг 8-10. Показано как удаляется вектор и его элементы
Когда вектор удаляется, всё его содержимое также удаляется: удаление вектора означает и удаление значений, которые он содержит. Средство проверки заимствования enum
SpreadsheetCell
{
Int(
i32
),
Float(
f64
),
Text(
String
),
} let row = vec!
[
SpreadsheetCell::Int(
3
),
SpreadsheetCell::Text(
String
::from(
"blue"
)),
SpreadsheetCell::Float(
10.12
),
];
{ let v = vec!
[
1
,
2
,
3
,
4
];
// do stuff with v
}
// <- v goes out of scope and is freed here
гарантирует, что любые ссылки на содержимое вектора используются только тогда, когда сам вектор действителен.
Давайте перейдём к следующему типу коллекции:
String
!

Сохранение текста с UTF-8 кодировкой в строках
Мы говорили о строках в главе 4, но сейчас мы рассмотрим их более подробно. Новички в Rust обычно застревают на строках из-за комбинации трёх причин: склонность Rust компилятора к выявлению возможных ошибок, более сложная структура данных чем считают многие программисты и UTF-8. Эти факторы объединяются таким образом, что тема может показаться сложной, если вы пришли из других языков программирования.
Полезно обсуждать строки в контексте коллекций, потому что строки реализованы в виде набора байтов, плюс некоторые методы для обеспечения полезной функциональности,
когда эти байты интерпретируются как текст. В этом разделе мы поговорим об операциях над
String таких как создание, обновление и чтение, которые есть у каждого типа коллекций. Мы также обсудим какими особенностями
String отличается от других коллекций, а именно каким образом индексирование в
String осложняется различием между тем как люди и компьютеры интерпретируют данные заключённые в
String
Что же такое строка?
Сначала мы определим, что мы подразумеваем под термином строка (string). В Rust есть только один строковый тип в ядре языка - срез строки str
, обычно используемый в заимствованном виде как
&str
. В Главе 4 мы говорили о срезах строк, string slices,
которые являются ссылками на некоторые строковые данные в кодировке UTF-8.
Например, строковые литералы хранятся в двоичном файле программы и поэтому являются срезами строк.
Тип
String предоставляемый стандартной библиотекой Rust, не встроен в ядро языка и является расширяемым, изменяемым, владеющим, строковым типом в UTF-8 кодировке.
Когда Rustaceans говорят о "строках" то, они обычно имеют ввиду типы
String или строковые срезы
&str
, а не просто один из них. Хотя этот раздел в основном посвящён
String
, оба типа интенсивно используются в стандартной библиотеке Rust, оба, и
String и строковые срезы, кодируются в UTF-8.
Создание новых строк
Многие из тех же операций, которые доступны
Vec
, доступны также в
String
,
потому что
String фактически реализован как обёртка вокруг вектора байтов с некоторыми дополнительными гарантиями, ограничениями и возможностями.
Примером функции, которая одинаково работает с
Vec
и
String
, является функция new
, создающая новый экземпляр типа, и показана в Листинге 8-11.
Листинг 8-11. Создание новой пустой
String
строки
let mut s =
String
::new();

Эта строка создаёт новую пустую строковую переменную с именем s
, в которую мы можем затем загрузить данные. Часто у нас есть некоторые начальные данные, которые мы хотим назначить строке. Для этого мы используем метод to_string доступный для любого типа, который реализует типаж
Display
, как у строковых литералов. Листинг 8-12
показывает два примера.
Листинг 8-12: Использование метода
to_string
для создания экземпляра типа
String
из строкового
литерала
Эти выражения создают строку с initial contents
Мы также можем использовать функцию
String::from для создания
String из строкового литерала. Код листинга 8-13 является эквивалентным коду из листинга 8-12,
который использует функцию to_string
:
Листинг 8-13: Использование функции
String::from
для создания экземпляра типа
String
из строкового
литерала
Поскольку строки используются для очень многих вещей, можно использовать множество API для строк, предоставляющих множество возможностей. Некоторые из них могут показаться избыточными, но все они занимаются своим делом! В данном случае
String::from и to_string делают одно и тоже, поэтому выбор зависит от стиля который вам больше импонирует.
Запомните, что строки хранятся в кодировке UTF-8, поэтому можно использовать любые правильно кодированные данные в них, как показано в листинге 8-14:
Листинг 8-14: Хранение приветствий в строках на разных языках
let data =
"initial contents"
; let s = data.to_string();
// the method also works on a literal directly:
let s =
"initial contents"
.to_string(); let s =
String
::from(
"initial contents"
); let hello =
String
::from(
"ﻢﻜﯿﻠﻋ مﻼﺴﻟا"
); let hello =
String
::from(
"Dobrý den"
); let hello =
String
::from(
"Hello"
); let hello =
String
::from(
"םוֹלָשׁ"
); let hello =
String
::from(
"नम े"
); let hello =
String
::from(
"こんにちは"
); let hello =
String
::from(
"안녕하세요"
); let hello =
String
::from(
"你好"
); let hello =
String
::from(
"Olá"
); let hello =
String
::from(
"Здравствуйте"
); let hello =
String
::from(
"Hola"
);

Все это допустимые
String значения.
Обновление строковых данных
Строка
String может увеличиваться в размере, а её содержимое может меняться, по аналогии как содержимое
Vec
при вставке в него большего количества данных.
Кроме того, можно использовать оператор
+
или макрос format!
для объединения значений
String
1   ...   13   14   15   16   17   18   19   20   ...   62
Присоединение к строке с помощью push_str и pushМы можем нарастить String используя метод push_str который добавит в исходное значение новый строковый срез, как показано в листинге 8-15.Листинг 8-15: Добавление среза строки к String используя метод push_strПосле этих двух строк кода s будет содержать foobar. Метод push_str принимает строковый срез, потому что мы не всегда хотим владеть входным параметром.Например, код в листинге 8-16 показывает вариант, когда будет не желательно поведение, при котором мы не сможем использовать s2 после его добавления к содержимому значения переменной s1Листинг 8-16: Использование фрагмента строки после его добавления в состав другого StringЕсли метод push_str стал бы владельцем переменной s2, мы не смогли бы напечатать его значение в последней строке. Однако этот код работает так, как мы ожидали!Метод push принимает один символ в качестве параметра и добавляет его к String. Влистинге 8-17 показан код, добавляющий букву “l” к String используя метод pushЛистинг 8-17: Добавление одного символа в String значение используя pushВ результате s будет содержать lol let mut s = String::from("foo"); s.push_str("bar"); let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); let mut s = String::from("lo"); s.push('l');

Объединение строк с помощью оператора + или макроса format!
Часто хочется объединять две существующие строки. Один из возможных способов —
это использование оператора
+
из листинга 8-18:
Листинг 8-18: Использование оператора
+
для объединения двух значений
String
в новое
String
значение
Строка s3
будет содержать
Hello, world!
. Причина того, что s1
после добавления больше недействительна и причина, по которой мы использовали ссылку на s2
имеют отношение к сигнатуре вызываемого метода при использовании оператора
+
. Оператор
+
использует метод add
, чья сигнатура выглядит примерно так:
В стандартной библиотеке вы увидите метод add определённым с использованием обобщённых и связанных типов. Здесь мы видим сигнатуру с конкретными типами,
заменяющими обобщённый, что происходит когда вызывается данный метод со значениями
String
. Мы обсудим обобщённые типы в Главе 10. Эта сигнатура даёт нам ключ для понимания особенностей оператора
+
Во-первых, перед s2
мы видим
&
, что означает что мы складываем ссылку на вторую строку с первой строкой. Это происходит из-за параметра s
в функции add
: мы можем добавить только
&str к
String
; мы не можем сложить два значения
String
. Но подождите — тип
&s2
это
&String
, а не
&str
, как определён второй параметр в add
Так почему код в листинге 8-18 компилируется?
Причина, по которой мы можем использовать
&s2
в вызове add заключается в том, что компилятор может принудительно привести (coerce) аргумент типа
&String к типу
&str
Когда мы вызываем метод add в Rust используется принудительное приведение (deref coercion), которое превращает
&s2
в
&s2[..]
. Мы подробно обсудим принудительное приведение в Главе 15. Так как add не забирает во владение параметр s
, s2
по прежнему будет действительной строкой
String после применения операции.
Во-вторых, как можно видеть в сигнатуре, add забирает во владение self
, потому что self
не имеет
&
. Это означает, что s1
в листинге 8-18 будет перемещён в вызов add и
больше не будет действителен после этого вызова. Не смотря на то, что код let s3 = s1
+ &s2;
выглядит как будто он скопирует обе строки и создаёт новую, это выражение фактически забирает во владение переменную s1
, присоединяет к ней копию содержимого s2
, а затем возвращает владение результатом. Другими словами, это выглядит как будто код создаёт множество копий, но это не так; данная реализация более эффективна чем копирование.
let s1 =
String
::from(
"Hello, "
); let s2 =
String
::from(
"world!"
); let s3 = s1 + &s2;
// note s1 has been moved here and can no longer be used fn add
(
self
, s: &
str
) ->
String
{

Если нужно объединить несколько строк, поведение оператора
+
становится громоздким:
Здесь переменная 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
];
Источник: https://files.student-it.ru/previewfile/264058