ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1146
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Разделение модулей на разные файлы
До сих пор все примеры в этой главе определяли несколько модулей в одном файле.
Когда модули становятся большими, вы можете захотеть переместить их определения в отдельные файлы, чтобы упростить навигацию по коду.
Например, давайте начнём с кода из листинга 7-17, в котором было несколько модулей ресторана. Мы будем извлекать модули в файлы вместо того, чтобы определять все модули в корневом модуле крейта. В нашем случае корневой модуль крейта - src/lib.rs, но это разделение также работает и с бинарными крейтами, у которых корневой модуль крейта — src/main.rs.
Сначала мы извлечём модуль front_of_house в свой собственный файл. Удалите код внутри фигурных скобок для модуля front_of_house
, оставив только объявление mod front_of_house;
, так что теперь src/lib.rs содержит код, показанный в листинге 7-21.
Обратите внимание, что этот вариант не скомпилируется, пока мы не создадим файл
src/front_of_house.rs из листинге 7-22.
Файл: src/lib.rs
Листинг 7-21. Объявление модуля
front_of_house
, чьё содержимое будет в src/front_of_house.rs
Затем поместим код, который был в фигурных скобках, в новый файл с именем
src/front_of_house.rs, как показано в листинге 7-22. Компилятор знает, что нужно искать в этом файле, потому что он наткнулся в корневом модуле крейта на объявление модуля с именем front_of_house
Файл: src/front_of_house.rs
Листинг 7-22. Определение содержимого модуля
front_of_house
в файле src/front_of_house.rs
Обратите внимание, что вам нужно только один раз загрузить файл с помощью объявления mod в вашем дереве модулей. Как только компилятор узнает, что файл является частью проекта (и узнает, где в дереве модулей находится код из-за того, куда вы поместили оператор mod
), другие файлы в вашем проекте должны ссылаться на код загруженного файла, используя путь к месту, где он был объявлен, как описано в разделе mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant
() { hosting::add_to_waitlist();
} pub mod hosting { pub fn add_to_waitlist
() {}
}
«Пути для ссылки на элемент в дереве модулей»
. Другими словами, mod
— это не
операция «включения», которую вы могли видеть в других языках программирования.
Далее мы извлечём модуль hosting в его собственный файл. Процесс немного отличается, потому что hosting является дочерним модулем для front_of_house
, а не корневого модуля. Мы поместим файл для hosting в новый каталог, который будет назван по имени его предка в дереве модулей, в данном случае это src/front_of_house/.
Чтобы начать перенос hosting
, мы меняем src/front_of_house.rs так, чтобы он содержал только объявление модуля hosting
:
Файл: src/front_of_house.rs
Затем мы создаём каталог src/front_of_house и файл hosting.rs, в котором будут определения, сделанные в модуле hosting
:
Файл: src/front_of_house/hosting.rs
Если вместо этого мы поместим hosting.rs в каталог src, компилятор будет думать, что код в hosting.rs это модуль hosting
, объявленный в корне крейта, а не объявленный как дочерний модуль front_of_house
. Правила компилятора для проверки какие файлы содержат код каких модулей предполагают, что каталоги и файлы точно соответствуют дереву модулей.
Альтернативные пути к файлам
До сих пор мы рассматривали наиболее идиоматические пути к файлам,
используемые компилятором Rust, но Rust также поддерживает и старый стиль пути к файлу. Для модуля с именем front_of_house
, объявленного в корневом модуле крейта, компилятор будет искать код модуля в:
src/front_of_house.rs (что мы рассматривали)
src/front_of_house/mod.rs (старый стиль, всё ещё поддерживаемый путь)
Для модуля с именем hosting
, который является подмодулем front_of_house
,
компилятор будет искать код модуля в:
src/front_of_house/hosting.rs (что мы рассматривали)
src/front_of_house/hosting/mod.rs (старый стиль, всё ещё поддерживаемый путь)
pub mod hosting; pub fn add_to_waitlist
() {}
Если вы используете оба стиля для одного и того же модуля, вы получите ошибку компилятора. Использование сочетания обоих стилей для разных модулей в одном проекте разрешено, но это может сбивать с толку людей, перемещающихся по вашему проекту.
Основным недостатком стиля, в котором используются файлы с именами 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
!
Давайте перейдём к следующему типу коллекции:
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
{