ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1187
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 13-9: Использование замыкания
FnMut
с
sort_by_key
разрешено
Трейты
Fn важны при определении или использовании функций или типов,
использующих замыкания. В следующем разделе мы обсудим итераторы. Многие методы итераторов принимают аргументы замыкания, поэтому не забывайте об этих деталях замыкания, по мере того как мы продвигаемся дальше!
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let mut list = [
Rectangle { width:
10
, height:
1
},
Rectangle { width:
3
, height:
5
},
Rectangle { width:
7
, height:
12
},
]; let mut num_sort_operations =
0
; list.sort_by_key(|r| { num_sort_operations +=
1
; r.width
}); println!
(
"{:#?}, sorted in {num_sort_operations} operations"
, list);
}
1 ... 30 31 32 33 34 35 36 37 ... 62
Обработка группы элементов с помощью итераторов
Шаблон итератора позволяет выполнять некоторые задачи над последовательностью элементов. Итератор отвечает за логику итерации по каждому элементу и определяет,
когда последовательность завершилась. Когда вы используете итераторы, вам не нужно переопределять эту логику самостоятельно.
В Rust итераторы являются lazy, то есть они не производят никакого эффекта, пока вы не вызовете методы, которые потребляют итератор, чтобы использовать его. Например, код в листинге 13-10 создаёт итератор элементов вектора v1
, вызывая метод iter
,
определённый для
Vec
. Сам по себе этот код не делает ничего полезного.
Листинг 13-10: Создание итератора
Итератор хранится в переменной v1_iter
. Создав итератор, мы можем использовать его различными способами. В листинге 3-5 главы 3 мы выполняли итерацию по массиву с помощью цикла for для выполнения какого-то кода над каждым из его элементов. Под капотом это неявно создавало, а затем потребляло итератор, но до сих пор мы не касались того, как именно это работает.
В примере в листинге 13-11 мы отделяем создание итератора от его использования в цикле for
. Когда цикл for вызывается используя итератор v1_iter
, то для каждого элемента итератора отводится одна итерация цикла, в ходе которой выводится каждое значение.
Листинг 13-11: Использование итератора в цикле
for
В языках, которые не имеют итераторов в стандартной библиотеке, вы, вероятно,
написали бы эту же функцию следующим образом: взять переменную со значением 0,
использовать её для индексации вектора, чтобы получить значение, и увеличивать её
значение в цикле, пока не будет достигнуто общее количество элементов в векторе.
Итераторы делают все эти шаги за вас, сокращая повторяющийся код, который вы потенциально могли бы испортить. Итераторы дают вам больше гибкости для использования одной и той же логики с различными типами последовательностей, а не let v1 = vec!
[
1
,
2
,
3
]; let v1_iter = v1.iter(); let v1 = vec!
[
1
,
2
,
3
]; let v1_iter = v1.iter(); for val in v1_iter { println!
(
"Got: {}"
, val);
}
только со структурами данных, которые можно индексировать, типа векторов. Давайте посмотрим как итераторы это делают.
Типаж Iterator и метод next
Все итераторы реализуют типаж
Iterator
, который определён в стандартной библиотеке. Его определение выглядит так:
Обратите внимание данное объявление использует новый синтаксис: type Item и
Self::Item
, которые определяют ассоциированный тип (associated type) с этим типажом.
Мы подробнее поговорим о ассоциированных типах в главе 19. Сейчас вам нужно знать,
что этот код требует от реализаций типажа
Iterator определить требуемый им тип
Item и данный тип
Item используется в методе next
. Другими словами, тип
Item будет являться типом элемента, который возвращает итератор.
Типаж
Iterator требует, чтобы разработчики определяли только один метод: метод next
, который возвращает один элемент итератора за раз обёрнутый в вариант
Some и
когда итерация завершена, возвращает
None
Мы можем вызывать метод next у итераторов напрямую; в листинге 13-12 показано,
какие значения возвращаются при повторных вызовах next у итератора, созданного из вектора.
Файл: src/lib.rs
Листинг 13-12: Вызов метода
next
итератора
pub trait
Iterator
{ type
Item
; fn next
(&
mut self
) ->
Option
<:item>;
// methods with default implementations elided
}
#[test]
fn iterator_demonstration
() { let v1 = vec!
[
1
,
2
,
3
]; let mut v1_iter = v1.iter(); assert_eq!
(v1_iter.next(),
Some
(&
1
)); assert_eq!
(v1_iter.next(),
Some
(&
2
)); assert_eq!
(v1_iter.next(),
Some
(&
3
)); assert_eq!
(v1_iter.next(),
None
);
}
Типаж Iterator и метод next
Все итераторы реализуют типаж
Iterator
, который определён в стандартной библиотеке. Его определение выглядит так:
Обратите внимание данное объявление использует новый синтаксис: type Item и
Self::Item
, которые определяют ассоциированный тип (associated type) с этим типажом.
Мы подробнее поговорим о ассоциированных типах в главе 19. Сейчас вам нужно знать,
что этот код требует от реализаций типажа
Iterator определить требуемый им тип
Item и данный тип
Item используется в методе next
. Другими словами, тип
Item будет являться типом элемента, который возвращает итератор.
Типаж
Iterator требует, чтобы разработчики определяли только один метод: метод next
, который возвращает один элемент итератора за раз обёрнутый в вариант
Some и
когда итерация завершена, возвращает
None
Мы можем вызывать метод next у итераторов напрямую; в листинге 13-12 показано,
какие значения возвращаются при повторных вызовах next у итератора, созданного из вектора.
Файл: src/lib.rs
Листинг 13-12: Вызов метода
next
итератора
pub trait
Iterator
{ type
Item
; fn next
(&
mut self
) ->
Option
<:item>;
// methods with default implementations elided
}
#[test]
fn iterator_demonstration
() { let v1 = vec!
[
1
,
2
,
3
]; let mut v1_iter = v1.iter(); assert_eq!
(v1_iter.next(),
Some
(&
1
)); assert_eq!
(v1_iter.next(),
Some
(&
2
)); assert_eq!
(v1_iter.next(),
Some
(&
3
)); assert_eq!
(v1_iter.next(),
None
);
}
Обратите внимание, что нам нужно сделать переменную v1_iter изменяемой: вызов метода next итератора изменяет внутреннее состояние итератора, которое итератор использует для отслеживания того, где он находится в последовательности. Другими словами, этот код потребляет (consumes) или использует итератор. Каждый вызов next потребляет элемент из итератора. Нам не нужно было делать изменяемой v1_iter при использовании цикла for
, потому что цикл забрал во владение v1_iter и сделал её
изменяемой неявно для нас.
Заметьте также, что значения, которые мы получаем при вызовах next являются неизменяемыми ссылками на значения в векторе. Метод iter создаёт итератор по неизменяемым ссылкам. Если мы хотим создать итератор, который становится владельцем v1
и возвращает принадлежащие ему значения, мы можем вызвать into_iter вместо iter
. Точно так же, если мы хотим перебирать изменяемые ссылки,
мы можем вызвать iter_mut вместо iter
Методы, которые потребляют итератор
У типажа
Iterator есть несколько методов, реализация которых по умолчанию предоставляется стандартной библиотекой; вы можете узнать об этих методах,
просмотрев документацию API стандартной библиотеки для
Iterator
. Некоторые из этих методов вызывают next в своём определении, поэтому вам необходимо реализовать метод next при реализации типажа
Iterator
Методы, вызывающие next
, называются потребляющими адаптерами, поскольку их вызов потребляет итератор. Примером может служить метод sum
, который забирает во владение итератор и перебирает элементы, многократно вызывая next
, тем самым потребляя итератор. В процессе итерации он добавляет каждый элемент к текущей сумме и возвращает итоговое значение по завершении итерации. В листинге 13-13
приведён тест, иллюстрирующий использование метода sum
:
Файл: src/lib.rs
Листинг 13-13: Вызов метода
sum
для получения суммы всех элементов в итераторе
#[test]
fn iterator_sum
() { let v1 = vec!
[
1
,
2
,
3
]; let v1_iter = v1.iter(); let total: i32
= v1_iter.sum(); assert_eq!
(total,
6
);
}
Мы не можем использовать v1_iter после вызова метода sum
, потому что sum забирает по владение итератор у которого вызван метод.
Методы, которые создают другие итераторы
Адаптеры итераторов - это методы, определённые для трейта
Iterator
, которые не потребляют итератор. Вместо этого они создают различные итераторы, изменяя некоторые аспекты исходного итератора.
В листинге 13-17 показан пример вызова метода адаптера итератора map
, который принимает замыкание и вызывает его для каждого элемента по мере итерации элементов. Метод map возвращает новый итератор, который создаёт изменённые элементы. Замыкание здесь создаёт новый итератор, в котором каждый элемент из вектора будет увеличен на 1:
Файл: src/main.rs
Листинг 13-14: Вызов адаптера итератора
map
для создания нового итератора
Однако этот код выдаёт предупреждение:
Код в листинге 13-14 ничего не делает; указанное нами замыкание никогда не вызывается. Предупреждение напоминает нам, почему: адаптеры итераторов ленивы, и здесь нам нужно использовать итератор.
Чтобы устранить это предупреждение и использовать итератор, мы воспользуемся методом collect
, который мы использовали в главе 12 с env::args в листинге 12-1. Этот метод потребляет итератор и собирает полученные значения в тип данных collection.
let v1:
Vec
<
i32
> = vec!
[
1
,
2
,
3
]; v1.iter().map(|x| x +
1
);
$
cargo run
Compiling iterators v0.1.0 (file:///projects/iterators) warning: unused `Map` that must be used
-->
src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: iterators are lazy and do nothing unless consumed warning: `iterators` (bin "iterators") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
В листинге 13-15 мы собираем в вектор результаты итерирования по итератору, который возвращается в результате вызова map
. Этот вектор в итоге будет содержать каждый элемент исходного вектора, увеличенный на 1.
Файл: src/main.rs
Листинг 13-15: Вызов метода
map
для создания нового итератора, а затем вызов метода
collect
для
потребления нового итератора и создания вектора
Поскольку map принимает замыкание, мы можем указать любую операцию, которую хотим выполнить с каждым элементом. Это отличный пример того, как замыкания позволяют настраивать какое-то поведение при повторном использовании итерационного поведения, предоставляемого типажом
Iterator
Вы можете выстроить цепочку из нескольких вызовов адаптеров итератора для выполнения сложных действий в удобочитаемом виде. Но поскольку все итераторы являются "ленивыми", для получения результатов вызовов адаптеров итератора необходимо вызвать один из методов потребляющего адаптера.
Использование замыканий, которые захватывают переменные
окружения
Многие адаптеры итераторов принимают замыкания в качестве аргументов, и обычно замыкания, которые мы будем указывать в качестве аргументов адаптерам итераторов,
это замыкания, которые фиксируют своё окружение.
В этом примере мы будем использовать метод filter
, который принимает замыкание.
Замыкание получает элемент из итератора и возвращает bool
. Если замыкание возвращает true
, значение будет включено в итерацию, создаваемую filter
. Если замыкание возвращает false
, значение не будет включено.
В листинге 13-16 мы используем filter с замыканием, которое захватывает переменную shoe_size из своего окружения для итерации по коллекции экземпляров структуры
Shoe
. Он будет возвращать обувь только указанного размера.
Файл: src/lib.rs let v1:
Vec
<
i32
> = vec!
[
1
,
2
,
3
]; let v2:
Vec
<_> = v1.iter().map(|x| x +
1
).collect(); assert_eq!
(v2, vec!
[
2
,
3
,
4
]);
Листинг 13-16. Использование метода
filter
с замыканием, фиксирующим
shoe_size
Функция shoes_in_size принимает в качестве параметров вектор с экземплярами обуви и размер обуви, а возвращает вектор, содержащий только обувь указанного размера.
В теле shoes_in_my_size мы вызываем into_iter чтобы создать итератор, который становится владельцем вектора. Затем мы вызываем filter
, чтобы превратить этот
#[derive(PartialEq, Debug)]
struct
Shoe
{ size: u32
, style:
String
,
} fn shoes_in_size
(shoes:
Vec
) ->
Vec
}
#[cfg(test)]
mod tests { use super::*;
#[test]
fn filters_by_size
() { let shoes = vec!
[
Shoe { size:
10
, style:
String
::from(
"sneaker"
),
},
Shoe { size:
13
, style:
String
::from(
"sandal"
),
},
Shoe { size:
10
, style:
String
::from(
"boot"
),
},
]; let in_my_size = shoes_in_size(shoes,
10
); assert_eq!
( in_my_size, vec!
[
Shoe { size:
10
, style:
String
::from(
"sneaker"
)
},
Shoe { size:
10
, style:
String
::from(
"boot"
)
},
]
);
}
}
итератор в другой, который содержит только элементы, для которых замыкание возвращает true
Замыкание захватывает параметр shoe_size из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect собирает значения, возвращаемые адаптированным итератором, в вектор,
возвращаемый функцией.
Тест показывает, что когда мы вызываем shoes_in_my_size
, мы возвращаем только туфли, размер которых совпадает с указанным нами значением.
Замыкание захватывает параметр shoe_size из окружения и сравнивает его с размером каждой пары обуви, оставляя только обувь указанного размера. Наконец, вызов collect собирает значения, возвращаемые адаптированным итератором, в вектор,
возвращаемый функцией.
Тест показывает, что когда мы вызываем shoes_in_my_size
, мы возвращаем только туфли, размер которых совпадает с указанным нами значением.
Улучшение проекта ввода/вывода
Вооружившись полученными знаниями об итераторах, мы можем улучшить реализацию работы с вводом-выводом в проекте главы 12, применяя итераторы для того, чтобы сделать некоторые места в коде более понятными и лаконичными. Давайте рассмотрим,
как итераторы могут улучшить нашу реализацию функции
Config::build и функции search
Удаление метода clone используя итератор
В листинге 12-6 мы добавили код, который принимает срез значений
String и создаёт экземпляр структуры
Config путём индексации среза и клонирования значений,
позволяя структуре
Config владеть этими значениями. В листинге 13-17 мы воспроизвели реализацию функции
Config::build
, как это было в листинге 12-23:
Файл: src/lib.rs
Листинг 13-17: Репродукция функции
Config::build
из листинга 12-23
Ранее мы говорили, что не стоит беспокоиться о неэффективных вызовах clone
, потому что мы удалим их в будущем. Ну что же, это время пришло!
Здесь нам понадобился clone
, потому что у нас есть срез с элементами
String в
параметре args
, но функция build не владеет args
. Чтобы вернуть владение экземпляру
Config
, нам пришлось клонировать значения полей query и filename из
Config
, чтобы экземпляр
Config мог владеть их значениями.
Благодаря нашим новым знаниям об итераторах мы можем изменить функцию build
,
чтобы вместо заимствования среза она принимала в качестве аргумента итератор. Мы impl
Config { pub fn build
(args: &[
String
]) ->
Result
> { if args.len() <
3
{ return
Err
(
"not enough arguments"
);
} let query = args[
1
].clone(); let file_path = args[
2
].clone(); let ignore_case = env::var(
"IGNORE_CASE"
).is_ok();
Ok
(Config { query, file_path, ignore_case,
})
}
}
будем использовать функциональность итератора вместо кода, который проверяет длину среза и обращается по индексу к определённым значениям. Это позволит лучше понять, что делает функция
Config::build
, поскольку итератор будет обращаться к значениям.
Как только
Config::build получит в своё распоряжение итератор и перестанет использовать операции индексирования с заимствованием, мы сможем переместить значения
String из итератора в
Config вместо того, чтобы вызывать clone и создавать новое выделение памяти.
Использование возвращённого итератора напрямую
Откройте файл src/main.rs проекта ввода-вывода, который должен выглядеть следующим образом:
Файл: src/main.rs
Сначала мы изменим начало функции main
, которая была в листинге 12-24, на код в листинге 13-18, который теперь использует итератор. Это не будет компилироваться,
пока мы не обновим
Config::build
Файл: src/main.rs
Листинг 13-18: Передача возвращаемого значения из
env::args
в
Config::build
Функция env::args возвращает итератор! Вместо того чтобы собирать значения итератора в вектор и затем передавать срез в
Config::build
, теперь мы передаём владение итератором, возвращённым из env::args в
Config::build напрямую.
fn main
() { let args:
Vec
<
String
> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
});
// --snip--
} fn main
() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
});
// --snip--
}
Config::build
, поскольку итератор будет обращаться к значениям.
Как только
Config::build получит в своё распоряжение итератор и перестанет использовать операции индексирования с заимствованием, мы сможем переместить значения
String из итератора в
Config вместо того, чтобы вызывать clone и создавать новое выделение памяти.
Использование возвращённого итератора напрямую
Откройте файл src/main.rs проекта ввода-вывода, который должен выглядеть следующим образом:
Файл: src/main.rs
Сначала мы изменим начало функции main
, которая была в листинге 12-24, на код в листинге 13-18, который теперь использует итератор. Это не будет компилироваться,
пока мы не обновим
Config::build
Файл: src/main.rs
Листинг 13-18: Передача возвращаемого значения из
env::args
в
Config::build
Функция env::args возвращает итератор! Вместо того чтобы собирать значения итератора в вектор и затем передавать срез в
Config::build
, теперь мы передаём владение итератором, возвращённым из env::args в
Config::build напрямую.
fn main
() { let args:
Vec
<
String
> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
});
// --snip--
} fn main
() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!(
"Problem parsing arguments: {err}"
); process::exit(
1
);
});
// --snip--
}
Далее нам нужно обновить определение
Config::build
. В файле src/lib.rs вашего проекта ввода-вывода изменим сигнатуру
Config::build так, чтобы она выглядела как в листинге 13-19. Это все ещё не скомпилируется, потому что нам нужно обновить тело функции.
Файл: src/lib.rs
Листинг 13-19: Обновление сигнатуры
Config::build
для определения итератора как ожидаемого
параметра
Документация стандартной библиотеки для функции env::args показывает, что тип возвращаемого ею итератора - std::env::Args
, и этот тип реализует признак
Iterator и
возвращает значения
String
Мы обновили сигнатуру функции
Config::build
, чтобы параметр args имел универсальный тип ограниченный трейтом impl Iterator
вместо
&
[String]
. Такое использование синтаксиса impl Trait
, который мы обсуждали в разделе " Трейты как параметры"
главы 10, означает, что args может быть любым типом,
реализующим тип
Iterator и возвращающим элементы
String
Поскольку мы владеем args и будем изменять args в процессе итерации над ним, мы можем добавить ключевое слово mut в спецификацию параметра args
, чтобы сделать его изменяемым.
Использование методов типажа Iterator вместо индексов
Далее мы подправим содержимое
Config::build
. Поскольку args реализует признак
Iterator
, мы знаем, что можем вызвать у него метод next
! В листинге 13-20 код из листинга 12-23 обновлён для использования метода next
:
Файл: src/lib.rs impl
Config { pub fn build
( mut args: impl
Iterator
>,
) ->
Result
> {
// --snip--
Листинг 13-20: Изменяем тело
Config::build
так, чтобы использовать методы итератора
Помните, что первое значение в возвращаемых данных env::args
- это имя программы.
Мы хотим проигнорировать его и перейти к следующему значению, поэтому сперва мы вызываем next и ничего не делаем с возвращаемым значением. Затем мы вызываем next
, чтобы получить значение, которое мы хотим поместить в поле query в
Config
Если next возвращает
Some
, мы используем match для извлечения значения. Если возвращается
None
, это означает, что было задано недостаточно аргументов, и мы досрочно возвращаем значение
Err
. То же самое мы делаем для значения filename
1 ... 31 32 33 34 35 36 37 38 ... 62
Делаем код понятнее с помощью адаптеров итераторов
Мы также можем воспользоваться преимуществами итераторов в функции search в
нашем проекте с операциями ввода-вывода, которая воспроизведена здесь в листинге
13-21 так же, как и в листинге 12-19:
Файл: src/lib.rs impl
Config { pub fn build
( mut args: impl
Iterator
>,
) ->
Result
> { args.next(); let query = match args.next() {
Some
(arg) => arg,
None
=> return
Err
(
"Didn't get a query string"
),
}; let file_path = match args.next() {
Some
(arg) => arg,
None
=> return
Err
(
"Didn't get a file path"
),
}; let ignore_case = env::var(
"IGNORE_CASE"
).is_ok();
Ok
(Config { query, file_path, ignore_case,
})
}
}
Листинг 13-21: Реализация функции
search
из листинга 12-19
Мы можем написать этот код в более сжатом виде, используя методы адаптера итератора. Это также позволит нам избежать наличия изменяемого временного вектора results
. Функциональный стиль программирования предпочитает минимизировать количество изменяемого состояния, чтобы сделать код более понятным. Удаление изменяемого состояния может позволить в будущем сделать поиск параллельным,
поскольку нам не придётся управлять одновременным доступом к вектору results
. В
листинге 13-22 показано это изменение:
Файл: src/lib.rs
Листинг 13-22: Использование методов адаптера итератора в реализации функции
search
Напомним, что назначение функции search
- вернуть все строки в contents
, которые содержат query
. Подобно примеру filter в листинге 13-16, этот код использует адаптер filter
, чтобы сохранить только те строки, для которых line.contains(query)
возвращает true
. Затем мы собираем совпадающие строки в другой вектор с помощью collect
. Так гораздо проще! Не стесняйтесь сделать такое же изменение для использования методов итератора в функции search_case_insensitive
Выбор между циклами или итераторами
Следующий логичный вопрос - какой стиль вы должны выбрать в своём коде и почему:
оригинальную реализацию в листинге 13-21 или версию с использованием итераторов в листинге 13-22. Большинство программистов на языке Rust предпочитают использовать стиль итераторов. Сначала разобраться с ним немного сложно, но как только вы почувствуете, что такое различные адаптеры итераторов и что они делают, понять итераторы станет проще. Вместо того чтобы возиться с различными элементами цикла и pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { let mut results =
Vec
::new(); for line in contents.lines() { if line.contains(query) { results.push(line);
}
} results
} pub fn search
<
'a
>(query: &
str
, contents: &
'a str
) ->
Vec
<&
'a str
> { contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
создавать новые векторы, код фокусируется на высокоуровневой цели цикла. Это абстрагирует часть обычного кода, поэтому легче увидеть концепции, уникальные для этого кода, такие как условие фильтрации, которое должен пройти каждый элемент в итераторе.
Но действительно ли эти две реализации эквивалентны? Интуитивно можно предположить, что более низкоуровневый цикл будет быстрее. Давайте поговорим о производительности.
Но действительно ли эти две реализации эквивалентны? Интуитивно можно предположить, что более низкоуровневый цикл будет быстрее. Давайте поговорим о производительности.
Сравнение производительности циклов и итераторов
Чтобы определить, что лучше использовать циклы или итераторы, нужно знать, какая реализация быстрее: версия функции search с явным циклом for или версия с итераторами.
Мы выполнили тест производительности, разместив всё содержимое книги (“The
Adventures of Sherlock Holmes” by Sir Arthur Conan Doyle) в строку типа
String и поискали слово the в её содержимом. Вот результаты теста функции search с использованием цикла for и с использованием итераторов:
Версия с использованием итераторов была немного быстрее! Мы не будем приводить здесь непосредственно код теста, поскольку идея не в том, чтобы доказать, что решения в точности эквивалентны, а в том, чтобы получить общее представление о том, как эти две реализации близки по производительности.
Для более исчерпывающего теста, вам нужно проверить различные тексты разных размеров в качестве содержимого для contents
, разные слова и слова различной длины в качестве query и всевозможные другие варианты. Дело в том, что итераторы, будучи высокоуровневой абстракцией, компилируются примерно в тот же код, как если бы вы написали его низкоуровневый вариант самостоятельно. Итераторы - это одна из
абстракций с нулевой стоимостью ( zero-cost abstractions ) в Rust, под которой мы подразумеваем, что использование абстракции не накладывает дополнительных расходов во время выполнения. Аналогично тому, как Бьёрн Страуструп, дизайнер и разработчик C++, определяет нулевые накладные расходы ( zero-overhead ) в книге
“Foundations of C++” (2012):
В целом, реализация C++ подчиняется принципу отсутствия накладных расходов: за то, чем вы не пользуетесь, платить не нужно. И далее: тот код, что вы используете,
нельзя сделать ещё лучше.
В качестве другого примера приведём код, взятый из аудио декодера. Алгоритм декодирования использует математическую операцию линейного предсказания для оценки будущих значений на основе линейной функции предыдущих выборок. Код использует комбинирование вызовов итератора для выполнения математических вычислений для трёх переменных в области видимости: срез данных buffer
, массив из
12 коэффициентов coefficients и число для сдвига данных в переменной qlp_shift
Переменные определены в примере, но не имеют начальных значений. Хотя этот код не имеет большого значения вне контекста, он является кратким, реальным примером того,
как Rust переводит идеи высокого уровня в код низкого уровня.
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
Чтобы вычислить значение переменной prediction
, этот код перебирает каждое из 12
значений в переменной coefficients и использует метод zip для объединения значений коэффициентов с предыдущими 12 значениями в переменной buffer
. Затем,
для каждой пары мы перемножаем значения, суммируем все результаты и у суммы сдвигаем биты вправо в переменную qlp_shift
Для вычислений в таких приложениях, как аудио декодеры, часто требуется производительность. Здесь мы создаём итератор, используя два адаптера, впоследствии потребляющих значение. В какой ассемблерный код будет компилироваться этот код на
Rust? На момент написания этой главы он компилируется в то же самое, что вы написали бы руками. Не существует цикла, соответствующего итерации по значениям в
«коэффициентах»
coefficients
: Rust знает, что существует двенадцать итераций,
поэтому он «разворачивает» цикл. Разворачивание - это оптимизация, которая устраняет издержки кода управления циклом и вместо этого генерирует повторяющийся код для каждой итерации цикла.
Все коэффициенты сохраняются в регистрах, что означает очень быстрый доступ к значениям. Нет никаких проверок границ доступа к массиву во время выполнения. Все эти оптимизации, которые может применить Rust, делают полученный код чрезвычайно эффективным. Теперь, когда вы это знаете, используйте итераторы и замыкания без страха! Они представляют код в более высокоуровневом виде, но без потери производительности во время выполнения.
Итоги
Замыкания (closures) и итераторы (iterators) это возможности Rust, вдохновлённые идеями функциональных языков. Они позволяют Rust ясно выражать идеи высокого уровня с производительностью низкоуровневого кода. Реализации замыканий и итераторов таковы, что нет влияния на производительность выполнения кода. Это одна из целей Rust, направленных на обеспечение абстракций с нулевой стоимостью (zero- cost abstractions).
let buffer: &
mut
[
i32
]; let coefficients: [
i64
;
12
]; let qlp_shift: i16
; for i in
12
..buffer.len() { let prediction = coefficients.iter()
.zip(&buffer[i -
12
..i])
.map(|(&c, &s)| c * s as i64
)
.sum::<
i64
>() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32
+ delta;
}
Теперь, когда мы улучшили представление кода в нашем проекте, рассмотрим некоторые возможности, которые нам предоставляет cargo для публикации нашего кода в репозитории.
Больше о Cargo и Crates.io
До сих пор мы использовали только самые основные возможности Cargo для сборки,
запуска и тестирования нашего кода, но он может гораздо больше. В этой главе мы обсудим некоторые другие, более продвинутые возможности, чтобы показать вам, как делать следующее:
Настройка сборки с помощью релизных профилей
Публикация библиотеки на crates.io
Управление крупными проектами с помощью рабочих пространств
Установка бинарных файлов с crates.io
Расширение возможностей Cargo с помощью возможности добавления собственных команд
Cargo может делать значительно больше того, что мы рассмотрим в этой главе, полное описание всех его функций см. в документации
Настройка сборок с профилями релизов
В Rust профили выпуска — это предопределённые и настраиваемые профили с различными конфигурациями, которые позволяют программисту лучше контролировать различные параметры компиляции кода. Каждый профиль настраивается независимо от других.
Cargo имеет два основных профиля: профиль dev
, используемый Cargo при запуске cargo build
, и профиль release
, используемый Cargo при запуске cargo build -- release
. Профиль dev определён со значениями по умолчанию для разработки, а профиль release имеет значения по умолчанию для сборок в релиз.
Эти имена профилей могут быть знакомы по результатам ваших сборок:
dev и release
— это разные профили, используемые компилятором.
Cargo содержит настройки по умолчанию для каждого профиля, которые применяются,
если вы явно не указали секции
[profile.*]
в файле проекта Cargo.toml. Добавляя секции
[profile.*]
для любого профиля, который вы хотите настроить, вы переопределяете любое подмножество параметров по умолчанию. Например, вот значения по умолчанию для параметра opt-level для профилей dev и release
:
Файл: Cargo.toml
Параметр opt-level управляет количеством оптимизаций, которые Rust будет применять к вашему коду, в диапазоне от 0 до 3. Использование большего количества оптимизаций увеличивает время компиляции, поэтому если вы находитесь в процессе разработки и часто компилируете свой код, целесообразно использовать меньшее количество оптимизаций, чтобы компиляция происходила быстрее, даже если в результате код будет работать медленнее. Поэтому opt-level по умолчанию для dev установлен в
0
. Когда вы готовы опубликовать свой код, то лучше потратить больше времени на компиляцию. Вы скомпилируете программу в режиме релиза только один раз, но выполняться она будет многократно, так что использование режима релиза позволяет увеличить скорость выполнения кода за счёт времени компиляции. Вот почему по умолчанию opt-level для профиля release равен
3
$
cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$
cargo build --release
Finished release [optimized] target(s) in 0.0s
[profile.dev]
opt-level =
0
[profile.release]
opt-level =
3
Вы можете переопределить настройки по умолчанию, добавив другое значение для них в Cargo.toml. Например, если мы хотим использовать уровень оптимизации 1 в профиле разработки, мы можем добавить эти две строки в файл Cargo.toml нашего проекта:
Файл: Cargo.toml
Этот код переопределяет настройку по умолчанию
0
. Теперь, когда мы запустим cargo build
, Cargo будет использовать значения по умолчанию для профиля dev плюс нашу настройку для opt-level
. Поскольку мы установили для opt-level значение
1
, Cargo будет применять больше оптимизаций, чем было задано по умолчанию, но не так много,
как при сборке релиза.
Полный список параметров конфигурации и значений по умолчанию для каждого профиля вы можете найти в документации Cargo
[profile.dev]
opt-level =
1
Публикация библиотеки в Crates.io
Мы использовали пакеты из crates.io в качестве зависимостей нашего проекта, но вы также можете поделиться своим кодом с другими людьми, опубликовав свои собственные пакеты. Реестр библиотек по адресу crates.io распространяет исходный код ваших пакетов, поэтому он в основном размещает код с открытым исходным кодом.
В Rust и Cargo есть функции, которые облегчают поиск и использование опубликованного пакета. Далее мы поговорим о некоторых из этих функций, а затем объясним, как опубликовать пакет.
Создание полезных комментариев к документации
Аккуратное документирование ваших пакетов поможет другим пользователям знать, как и когда их использовать, поэтому стоит потратить время на написание документации. В
главе 3 мы обсуждали, как комментировать код Rust, используя две косые черты,
//
. В
Rust также есть особый вид комментариев к документации, который обычно называется
комментарием к документации, который генерирует документацию HTML. HTML-код отображает содержимое комментариев к документации для публичных элементов API,
предназначенных для программистов, заинтересованных в знании того, как
использовать вашу библиотеку, в отличие от того, как она реализована.
Комментарии к документации используют три слеша,
///
вместо двух и поддерживают нотацию Markdown для форматирования текста. Размещайте комментарии к документации непосредственно перед элементом, который они документируют. В
листинге 14-1 показаны комментарии к документации для функции add_one в
библиотеке с именем my_crate
:
Файл: src/lib.rs
Листинг 14-1: Комментарий к документации для функции
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one
(x: i32
) -> i32
{ x +
1
}
Здесь мы даём описание того, что делает функция add_one
, начинаем раздел с заголовка
Examples
, а затем предоставляем код, который демонстрирует, как использовать функцию add_one
. Мы можем сгенерировать документацию HTML из этого комментария к документации, запустив cargo doc
. Эта команда запускает инструмент rustdoc
,
поставляемый с Rust, и помещает сгенерированную HTML-документацию в каталог
target/doc.
Для удобства, запустив cargo doc --open
, мы создадим HTML для документации вашей текущей библиотеки (а также документацию для всех зависимостей вашей библиотеки) и откроем результат в веб-браузере. Перейдите к функции add_one и вы увидите, как отображается текст в комментариях к документации, что показано на рисунке 14-1:
Рисунок 14-1: HTML документация для функции
add_one
Часто используемые разделы
Мы использовали Markdown заголовок
# Examples в листинге 14-1 для создания раздела в HTML с заголовком "Examples". Вот некоторые другие разделы, которые авторы библиотек обычно используют в своей документации:
Panics: Сценарии, в которых документированная функция может вызывать панику.
Вызывающие функцию, которые не хотят, чтобы их программы паниковали, должны убедиться, что они не вызывают функцию в этих ситуациях.
Ошибки: Если функция возвращает
Result
, описание типов ошибок, которые могут произойти и какие условия могут привести к тому, что эти ошибки могут быть
возвращены, может быть полезным для вызывающих, так что они могут написать код для обработки различных типов ошибок разными способами.
Безопасность: Если функция является unsafe для вызова (мы обсуждаем безопасность в главе 19), должен быть раздел, объясняющий, почему функция небезопасна и охватывающий инварианты, которые функция ожидает от вызывающих сторон.
В подавляющем большинстве случаев комментарии к документации не нуждаются во всех этих разделах, но это хорошая подсказка, напоминающая вам о тех аспектах вашего кода, о которых пользователям будет интересно узнать.
Комментарии к документации как тесты
Добавление примеров кода в комментарии к документации может помочь продемонстрировать, как использовать вашу библиотеку, и это даёт дополнительный бонус: запуск cargo test запустит примеры кода в вашей документации как тесты! Нет ничего лучше, чем документация с примерами. Но нет ничего хуже, чем примеры,
которые не работают, потому что код изменился с момента написания документации.
Если мы запустим cargo test с документацией для функции add_one из листинга 14-1,
мы увидим раздел результатов теста, подобный этому:
Теперь, если мы изменим либо функцию, либо пример, так что assert_eq!
в примере паникует, и снова запустим cargo test
, мы увидим, что тесты документации обнаруживают, что пример и код не синхронизированы друг с другом!
Комментирование содержащихся элементов
Стиль комментариев к документам
//!
добавляет документацию к элементу,
содержащему комментарии, а не к элементам, следующим за комментариями. Обычно мы используем эти комментарии внутри корневого файла крейта (по соглашению
src/lib.rs ) или внутри модуля для документирования крейта или модуля в целом.
Например, чтобы добавить документацию, описывающую назначение my_crate
,
содержащего функцию add_one
, мы добавляем комментарии к документации,
начинающиеся с
//!
в начало файла src/lib.rs , как показано в листинге 14-2:
Файл: src/lib.rs
Doc-tests my_crate running 1 test test src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
Безопасность: Если функция является unsafe для вызова (мы обсуждаем безопасность в главе 19), должен быть раздел, объясняющий, почему функция небезопасна и охватывающий инварианты, которые функция ожидает от вызывающих сторон.
В подавляющем большинстве случаев комментарии к документации не нуждаются во всех этих разделах, но это хорошая подсказка, напоминающая вам о тех аспектах вашего кода, о которых пользователям будет интересно узнать.
Комментарии к документации как тесты
Добавление примеров кода в комментарии к документации может помочь продемонстрировать, как использовать вашу библиотеку, и это даёт дополнительный бонус: запуск cargo test запустит примеры кода в вашей документации как тесты! Нет ничего лучше, чем документация с примерами. Но нет ничего хуже, чем примеры,
которые не работают, потому что код изменился с момента написания документации.
Если мы запустим cargo test с документацией для функции add_one из листинга 14-1,
мы увидим раздел результатов теста, подобный этому:
Теперь, если мы изменим либо функцию, либо пример, так что assert_eq!
в примере паникует, и снова запустим cargo test
, мы увидим, что тесты документации обнаруживают, что пример и код не синхронизированы друг с другом!
Комментирование содержащихся элементов
Стиль комментариев к документам
//!
добавляет документацию к элементу,
содержащему комментарии, а не к элементам, следующим за комментариями. Обычно мы используем эти комментарии внутри корневого файла крейта (по соглашению
src/lib.rs ) или внутри модуля для документирования крейта или модуля в целом.
Например, чтобы добавить документацию, описывающую назначение my_crate
,
содержащего функцию add_one
, мы добавляем комментарии к документации,
начинающиеся с
//!
в начало файла src/lib.rs , как показано в листинге 14-2:
Файл: src/lib.rs
Doc-tests my_crate running 1 test test src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
Листинг 14-2: Документация для крейта
my_crate
в целом
Обратите внимание, что после последней строки, начинающейся с
//!
, нет никакого кода. Поскольку мы начали комментарии с
//!
вместо
///
, мы документируем элемент,
который содержит этот комментарий, а не элемент, который следует за этим комментарием. В данном случае таким элементом является файл src/lib.rs, который является корнем crate. Эти комментарии описывают весь крейт.
Когда мы запускаем cargo doc --open
, эти комментарии будут отображаться на первой странице документации для my_crate над списком публичных элементов в библиотеке,
как показано на рисунке 14-2:
1 ... 32 33 34 35 36 37 38 39 ... 62
Рисунок 14-2: Предоставленная документация для
my_crate
, включая комментарий, описывающие крейт в
целом
Комментарии к документации внутри элементов полезны для описания крейтов и модулей особенно. Используйте их, чтобы объяснить общую цель контейнера, чтобы помочь вашим пользователям понять организацию крейта.
Экспорт удобного общедоступного API с pub use
Структура вашего публичного API является основным фактором при публикации крейта.
Люди, которые используют вашу библиотеку, менее знакомы со структурой, чем вы и
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
могут столкнуться с трудностями при поиске частей, которые они хотят использовать,
если ваша библиотека имеет большую иерархию модулей.
В главе 7 мы рассмотрели, как сделать элементы общедоступными с помощью ключевого слова pub и ввести элементы в область видимости с помощью ключевого слова use
Однако структура, которая имеет смысл для вас при разработке крейта, может быть не очень удобной для пользователей. Вы можете организовать структуру в виде иерархии с несколькими уровнями, но тогда люди, желающие использовать тип, который вы определили в глубине иерархии, могут столкнуться с проблемой его поиска. Их также может раздражать необходимость вводить use my_crate::some_module::another_module::UsefulType;
вместо use my_crate::UsefulType;
Хорошей новостью является то, что если структура не удобна для использования другими из другой библиотеки, вам не нужно перестраивать внутреннюю организацию: вместо этого вы можете реэкспортировать элементы, чтобы сделать публичную структуру,
отличную от вашей внутренней структуры, используя pub use
. Реэкспорт берет открытый элемент в одном месте и делает его публичным в другом месте, как если бы он был определён в другом месте.
Например, скажем, мы создали библиотеку с именем art для моделирования художественных концепций. Внутри этой библиотеки есть два модуля: модуль kinds содержащий два перечисления с именами
PrimaryColor и
SecondaryColor и модуль utils
, содержащий функцию с именем mix
, как показано в листинге 14-3:
Файл: src/lib.rs
Листинг 14-3: Библиотека
art
с элементами, организованными в модули
kinds
и
utils
На рисунке 14-3 показано, как будет выглядеть титульная страница документации для этого крейта, сгенерированный cargo doc
:
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum
PrimaryColor
{
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum
SecondaryColor
{
Orange,
Green,
Purple,
}
} pub mod utils { use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix
(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
Рисунок 14-3: Первая страница документации для
art
, в которой перечислены модули
kinds
и
utils
Обратите внимание, что типы
PrimaryColor и
SecondaryColor не указаны на главной странице, равно как и функция mix
. Мы должны нажать kinds и utils
, чтобы увидеть их.
В другой библиотеке, которая зависит от этой библиотеки, потребуются операторы use
,
которые подключают элементы из art в область видимости, определяя структуру модуля, которая определена в данный момент. В листинге 14-4 показан пример крейта, в котором используются элементы
PrimaryColor и mix из крейта art
:
Файл: src/main.rs
Листинг 14-4: Крейт использующий элементы из крейта
art
с экспортированной внутренней структурой
Автору кода в листинге 14-4, который использует крейт art
, пришлось выяснить, что
PrimaryColor находится в модуле kinds
, а mix
- в модуле utils
. Структура модуля art крейта больше подходит для разработчиков, работающих над art крейтом, чем для тех,
кто его использует. Внутренняя структура не содержит никакой полезной информации для того, кто пытается понять, как использовать крейт art
, а скорее вызывает путаницу,
поскольку разработчики, использующие его, должны понять, где искать, и должны указывать имена модулей в выражениях use
Чтобы удалить внутреннюю организацию из общедоступного API, мы можем изменить код крейта art в листинге 14-3, чтобы добавить операторы pub use для повторного реэкспорта элементов на верхнем уровне, как показано в листинге 14-5:
Файл: src/lib.rs use art::kinds::PrimaryColor; use art::utils::mix; fn main
() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);
}
Листинг 14-5: Добавление операторов
pub use
для реэкспорта элементов
Документация API, которую cargo doc генерирует для этой библиотеки, теперь будет перечислять и связывать реэкспорты на главной странице, как показано на рисунке 14-4,
упрощая поиск типов
PrimaryColor
,
SecondaryColor и функции mix
Рисунок 14-4: Первая страница документации для
art
, которая перечисляет реэкспорт
Пользователи крейта art могут по-прежнему видеть и использовать внутреннюю структуру из листинга 14-3, как показано в листинге 14-4, или они могут использовать
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds {
// --snip--
} pub mod utils {
// --snip--
}
более удобную структуру в листинге 14-5, как показано в листинге 14-6:
Файл: src/main.rs
Листинг 14-6: Программа, использующая реэкспортированные элементы из крейта
art
В случаях, когда имеется много вложенных модулей, реэкспорт типов на верхнем уровне с помощью pub use может существенно повысить удобство работы для людей,
использующих крейт. Ещё одно распространённое использование pub use
- это реэкспорт определений зависимого модуля в текущем крейте, чтобы сделать определения этого крейта частью публичного API вашего крейта.
Создание полезной публичной структуры API - это больше искусство чем наука, и вы можете повторять, чтобы найти API, который лучше всего подойдёт вашим пользователям. Использование pub use даёт вам гибкость в том, как вы структурируете свою библиотеку внутри и отделяете эту внутреннюю структуру от того, что вы предоставляете пользователям. Посмотрите на код некоторых установленных крейтов,
чтобы увидеть отличается ли их внутренняя структура от их публичного API.
Настройка учётной записи Crates.io
Прежде чем вы сможете опубликовать любые библиотеки, вам необходимо создать учётную запись на crates.io и получить API токен. Для этого зайдите на домашнюю страницу crates.io и войдите в систему через учётную запись GitHub. (В настоящее время требуется наличие учётной записи GitHub, но сайт может поддерживать другие способы создания учётной записи в будущем.) Сразу после входа в систему перейдите в настройки своей учётной записи по адресу https://crates.io/me/
и получите свой ключ API. Затем выполните команду cargo login с вашим ключом API, например:
Эта команда сообщит Cargo о вашем API token и сохранит его локально в
/.cargo/credentials. Обратите внимание, что этот токен является секретным: не делитесь им ни с кем другим. Если вы по какой-либо причине поделитесь им с кем-либо, вы должны отозвать его и сгенерировать новый токен на crates.io
Добавление метаданных в новую библиотеку
use art::mix; use art::PrimaryColor; fn main
() {
// --snip--
}
$
cargo login abcdefghijklmnopqrstuvwxyz012345
Допустим, у вас есть крейт, который вы хотите опубликовать. Перед публикацией вам нужно добавить некоторые метаданные в раздел
[package]
файла Cargo.toml крейта.
Вашему крейту понадобится уникальное имя. Пока вы работаете над крейтом локально,
вы можете назвать его как угодно. Однако названия крейтов на crates.io фиксируются в момент первой публикации. Как только крейту присвоено название, никто другой не сможет опубликовать крейт с таким же именем. Перед тем как опубликовать крейт,
поищите название, которое вы хотите использовать. Если такое имя уже используется,
вам придётся подобрать другое и отредактировать поле name в файле Cargo.toml в разделе
[package]
, чтобы использовать новое имя в качестве публикуемого, например,
так:
Файл: Cargo.toml
Даже если вы выбрали уникальное имя, когда вы запустите cargo publish чтобы опубликовать крейт, вы получите предупреждение, а затем ошибку:
Это ошибка, потому что вам не хватает важной информации: необходимы описание и лицензия, чтобы люди знали, что делает ваш крейт и на каких условиях они могут его использовать. В поле Cargo.toml добавьте описание, состоящее из одного-двух предложений, поскольку оно будет появляться вместе с вашим крейтом в результатах поиска. Для поля license нужно указать значение идентификатора лицензии. В
Linux
Foundation's Software Package Data Exchange (SPDX)
перечислены идентификаторы,
которые можно использовать для этого значения. Например, чтобы указать, что вы лицензировали свой crate, используя лицензию MIT, добавьте идентификатор
MIT
:
Файл: Cargo.toml
[package]
name =
"guessing_game"
$
cargo publish
Updating crates.io index warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip-- error: failed to publish to registry at https://crates.io
Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust- lang.org/cargo/reference/manifest.html for how to upload metadata
[package]
name =
"guessing_game"
license =
"MIT"
Если вы хотите использовать лицензию, которая отсутствует в SPDX, вам нужно поместить текст этой лицензии в файл, включите файл в свой проект, а затем используйте license-file
, чтобы указать имя этого файла вместо использования ключа license
Руководство по выбору лицензии для вашего проекта выходит за рамки этой книги.
Многие люди в сообществе Rust лицензируют свои проекты так же, как и Rust, используя двойную лицензию
MIT OR Apache 2.0
. Эта практика демонстрирует, что вы также можете указать несколько идентификаторов лицензий, разделённых
OR
, чтобы иметь несколько лицензий для вашего проекта.
С добавлением уникального имени, версии, вашего описания и лицензии, файл
Cargo.toml для проекта, который готов к публикации может выглядеть следующим образом:
Файл: Cargo.toml
Документация Cargo описывает другие метаданные, которые вы можете указать, чтобы другие могли легче находить и использовать ваш крейт.
Публикация на Crates.io
Теперь, когда вы создали учётную запись, сохранили свой токен API, выбрали имя для своего крейта и указали необходимые метаданные, вы готовы к публикации! Публикация библиотеки загружает определённую версию в crates.io для использования другими.
Будьте осторожны, потому что публикация является перманентной операцией. Версия никогда не сможет быть перезаписана, а код не подлежит удалению. Одна из основных целей crates.io
- служить постоянным архивом кода, чтобы сборки всех проектов,
зависящих от crates из crates.io продолжали работать. Предоставление возможности удаления версий сделало бы выполнение этой цели невозможным. При этом количество версий крейтов, которые вы можете опубликовать, не ограничено.
Запустите команду cargo publish ещё раз. Сейчас эта команда должна выполниться успешно:
[package]
name =
"guessing_game"
version =
"0.1.0"
edition =
"2021"
description =
"A fun game where you guess what number the computer has chosen."
license =
"MIT OR Apache-2.0"
[dependencies]
Поздравляем! Теперь вы поделились своим кодом с сообществом Rust и любой может легко добавить вашу библиотеку в качестве зависимости их проекта.
Публикация новой версии существующей библиотеки
Когда вы внесли изменения в свой крейт и готовы выпустить новую версию, измените значение version
, указанное в вашем файле Cargo.toml и повторите публикацию.
Воспользуйтесь
Semantic Versioning rules
, чтобы решить, какой номер следующей версии подходит для ваших изменений. Затем запустите cargo publish
, чтобы загрузить новую версию.
Устранение устаревших версий с Crates.io с помощью cargo yank
Хотя вы не можете удалить предыдущие версии крейта, вы можете помешать любым будущим проектам добавлять его в качестве новой зависимости. Это полезно, когда версия крейта сломана по той или иной причине. В таких ситуациях Cargo поддерживает
выламывание (yanking) версии крейта.
Вычёркивание версии не позволяет новым проектам зависеть от этой версии, но при этом позволяет всем существующим проектам, зависящим от неё, продолжать работу. По сути, исключение означает, что все проекты с Cargo.lock не сломаются, а любые файлы
Cargo.lock, которые будут генерироваться в будущем, не смогут использовать исключённую версию.
Чтобы вычеркнуть версию крейта, в директории крейта, который вы опубликовали ранее, выполните команду cargo yank и укажите, какую версию вы хотите вычеркнуть.
Например, если мы опубликовали крейт под названием guessing_game версии 1.0.1 и хотим вычеркнуть её, в каталоге проекта для guessing_game мы выполним:
Добавив в команду
--undo
, вы также можете отменить выламывание и разрешить проектам начать зависеть от версии снова:
$
cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
$
cargo yank --vers 1.0.1
Updating crates.io index
Yank guessing_game:1.0.1
Вычёркивание не удаляет код. Оно не может, например, удалить случайно загруженные пароли. Если это произойдёт, вы должны немедленно сбросить эти пароли.
$
cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank guessing_game_:1.0.1
Рабочие пространства Cargo
В главе 12 мы создали пакет, который включал в себя бинарный и библиотечный крейты.
По мере развития вашего проекта может возникнуть ситуация, когда библиотечный крейт будет становиться все больше, и вы захотите разделить ваш пакет на несколько библиотечных крейтов. Cargo предоставляет функциональность под названием
workspaces, которая помогает управлять несколькими взаимосвязанными пакетами,
которые разрабатываются в тандеме.
Создание рабочего пространства
Workspace - это набор пакетов, которые используют один и тот же Cargo.lock и директорию для хранения результатов компиляции. Давайте создадим проект с использованием
workspace - мы будем использовать тривиальный код, чтобы сосредоточиться на структуре рабочего пространства. Существует несколько способов структурировать рабочую область, но мы покажем только один из них. У нас будет рабочая область,
содержащая двоичный файл и две библиотеки. Двоичный файл, который обеспечивает основную функциональность, будет зависеть от двух библиотек. Одна библиотека предоставит функцию add_one
, а вторая - add_two
. Эти три крейта будут частью одного
workspace. Начнём с создания каталога для рабочего окружения:
Далее в каталоге add мы создадим файл Cargo.toml, который будет определять конфигурацию всего рабочего окружения. В этом файле не будет секции
[package]
Вместо этого он будет начинаться с секции
[workspace]
, которая позволит нам добавить модули в рабочее пространство, указав путь к пакету с нашим бинарным крейтом; в данном случае этот путь - adder:
Файл: Cargo.toml
Затем мы создадим исполняемый крейт adder
, запустив команду cargo new в каталоге
add:
На этом этапе мы можем создать рабочее пространство, запустив команду cargo build
Файлы в каталоге add должны выглядеть следующим образом:
$
mkdir add
$
cd add
[workspace]
members = [
"adder"
,
]
$
cargo new adder
Created binary (application) `adder` package
Рабочая область содержит на верхнем уровне один каталог target, в который будут помещены скомпилированные артефакты; пакет adder не имеет собственного каталога
target. Даже если мы запустим cargo build из каталога adder, скомпилированные артефакты все равно окажутся в add/target, а не в add/adder/target. Cargo так определил директорию target в рабочем пространстве, потому что крейты в рабочем пространстве должны зависеть друг от друга. Если бы каждый крейт имел свой собственный каталог
target, каждому крейту пришлось бы перекомпилировать каждый из других крейтов в рабочем пространстве, чтобы поместить артефакты в свой собственный каталог target.
Благодаря совместному использованию единого каталога target крейты могут избежать ненужной перекомпиляции.
Добавление второго крейта в рабочее пространство
Далее давайте создадим ещё одного участника пакета в рабочей области и назовём его add_one
. Внесите изменения в Cargo.toml верхнего уровня так, чтобы указать путь
add_one в списке members
:
Файл: Cargo.toml
Затем сгенерируйте новый крейт библиотеки с именем add_one
:
Ваш каталог add должен теперь иметь следующие каталоги и файлы:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
[workspace]
members = [
"adder"
,
"add_one"
,
]
$
cargo new add_one --lib
Created library `add_one` package
В файле add_one/src/lib.rs добавим функцию add_one
:
Файл: add_one/src/lib.rs
Теперь мы можем сделать так, чтобы пакет adder с нашим исполняемым файлом зависел от пакета add_one
, содержащего нашу библиотеку. Сначала нам нужно добавить зависимость пути от add_one в adder/Cargo.toml.
Файл: adder/Cargo.toml
Cargo не исходит из того, что крейты в рабочем пространстве могут зависеть друг от друга, поэтому нам необходимо явно указать отношения зависимости.
Далее, давайте используем функцию add_one
(из крейта add_one
) в крейте adder
Откройте файл adder/src/main.rs и добавьте строку use в верхней части, чтобы ввести в область видимости новый библиотечный крейт add_one
. Затем измените функцию main для вызова функции add_one
, как показано в листинге 14-7.
Файл: adder/src/main.rs
1 ... 33 34 35 36 37 38 39 40 ... 62
если ваша библиотека имеет большую иерархию модулей.
В главе 7 мы рассмотрели, как сделать элементы общедоступными с помощью ключевого слова pub и ввести элементы в область видимости с помощью ключевого слова use
Однако структура, которая имеет смысл для вас при разработке крейта, может быть не очень удобной для пользователей. Вы можете организовать структуру в виде иерархии с несколькими уровнями, но тогда люди, желающие использовать тип, который вы определили в глубине иерархии, могут столкнуться с проблемой его поиска. Их также может раздражать необходимость вводить use my_crate::some_module::another_module::UsefulType;
вместо use my_crate::UsefulType;
Хорошей новостью является то, что если структура не удобна для использования другими из другой библиотеки, вам не нужно перестраивать внутреннюю организацию: вместо этого вы можете реэкспортировать элементы, чтобы сделать публичную структуру,
отличную от вашей внутренней структуры, используя pub use
. Реэкспорт берет открытый элемент в одном месте и делает его публичным в другом месте, как если бы он был определён в другом месте.
Например, скажем, мы создали библиотеку с именем art для моделирования художественных концепций. Внутри этой библиотеки есть два модуля: модуль kinds содержащий два перечисления с именами
PrimaryColor и
SecondaryColor и модуль utils
, содержащий функцию с именем mix
, как показано в листинге 14-3:
Файл: src/lib.rs
Листинг 14-3: Библиотека
art
с элементами, организованными в модули
kinds
и
utils
На рисунке 14-3 показано, как будет выглядеть титульная страница документации для этого крейта, сгенерированный cargo doc
:
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum
PrimaryColor
{
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum
SecondaryColor
{
Orange,
Green,
Purple,
}
} pub mod utils { use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix
(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
Рисунок 14-3: Первая страница документации для
art
, в которой перечислены модули
kinds
и
utils
Обратите внимание, что типы
PrimaryColor и
SecondaryColor не указаны на главной странице, равно как и функция mix
. Мы должны нажать kinds и utils
, чтобы увидеть их.
В другой библиотеке, которая зависит от этой библиотеки, потребуются операторы use
,
которые подключают элементы из art в область видимости, определяя структуру модуля, которая определена в данный момент. В листинге 14-4 показан пример крейта, в котором используются элементы
PrimaryColor и mix из крейта art
:
Файл: src/main.rs
Листинг 14-4: Крейт использующий элементы из крейта
art
с экспортированной внутренней структурой
Автору кода в листинге 14-4, который использует крейт art
, пришлось выяснить, что
PrimaryColor находится в модуле kinds
, а mix
- в модуле utils
. Структура модуля art крейта больше подходит для разработчиков, работающих над art крейтом, чем для тех,
кто его использует. Внутренняя структура не содержит никакой полезной информации для того, кто пытается понять, как использовать крейт art
, а скорее вызывает путаницу,
поскольку разработчики, использующие его, должны понять, где искать, и должны указывать имена модулей в выражениях use
Чтобы удалить внутреннюю организацию из общедоступного API, мы можем изменить код крейта art в листинге 14-3, чтобы добавить операторы pub use для повторного реэкспорта элементов на верхнем уровне, как показано в листинге 14-5:
Файл: src/lib.rs use art::kinds::PrimaryColor; use art::utils::mix; fn main
() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);
}
Листинг 14-5: Добавление операторов
pub use
для реэкспорта элементов
Документация API, которую cargo doc генерирует для этой библиотеки, теперь будет перечислять и связывать реэкспорты на главной странице, как показано на рисунке 14-4,
упрощая поиск типов
PrimaryColor
,
SecondaryColor и функции mix
Рисунок 14-4: Первая страница документации для
art
, которая перечисляет реэкспорт
Пользователи крейта art могут по-прежнему видеть и использовать внутреннюю структуру из листинга 14-3, как показано в листинге 14-4, или они могут использовать
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds {
// --snip--
} pub mod utils {
// --snip--
}
Файл: src/main.rs
Листинг 14-6: Программа, использующая реэкспортированные элементы из крейта
art
В случаях, когда имеется много вложенных модулей, реэкспорт типов на верхнем уровне с помощью pub use может существенно повысить удобство работы для людей,
использующих крейт. Ещё одно распространённое использование pub use
- это реэкспорт определений зависимого модуля в текущем крейте, чтобы сделать определения этого крейта частью публичного API вашего крейта.
Создание полезной публичной структуры API - это больше искусство чем наука, и вы можете повторять, чтобы найти API, который лучше всего подойдёт вашим пользователям. Использование pub use даёт вам гибкость в том, как вы структурируете свою библиотеку внутри и отделяете эту внутреннюю структуру от того, что вы предоставляете пользователям. Посмотрите на код некоторых установленных крейтов,
чтобы увидеть отличается ли их внутренняя структура от их публичного API.
Настройка учётной записи Crates.io
Прежде чем вы сможете опубликовать любые библиотеки, вам необходимо создать учётную запись на crates.io и получить API токен. Для этого зайдите на домашнюю страницу crates.io и войдите в систему через учётную запись GitHub. (В настоящее время требуется наличие учётной записи GitHub, но сайт может поддерживать другие способы создания учётной записи в будущем.) Сразу после входа в систему перейдите в настройки своей учётной записи по адресу https://crates.io/me/
и получите свой ключ API. Затем выполните команду cargo login с вашим ключом API, например:
Эта команда сообщит Cargo о вашем API token и сохранит его локально в
/.cargo/credentials. Обратите внимание, что этот токен является секретным: не делитесь им ни с кем другим. Если вы по какой-либо причине поделитесь им с кем-либо, вы должны отозвать его и сгенерировать новый токен на crates.io
Добавление метаданных в новую библиотеку
use art::mix; use art::PrimaryColor; fn main
() {
// --snip--
}
$
cargo login abcdefghijklmnopqrstuvwxyz012345
Допустим, у вас есть крейт, который вы хотите опубликовать. Перед публикацией вам нужно добавить некоторые метаданные в раздел
[package]
файла Cargo.toml крейта.
Вашему крейту понадобится уникальное имя. Пока вы работаете над крейтом локально,
вы можете назвать его как угодно. Однако названия крейтов на crates.io фиксируются в момент первой публикации. Как только крейту присвоено название, никто другой не сможет опубликовать крейт с таким же именем. Перед тем как опубликовать крейт,
поищите название, которое вы хотите использовать. Если такое имя уже используется,
вам придётся подобрать другое и отредактировать поле name в файле Cargo.toml в разделе
[package]
, чтобы использовать новое имя в качестве публикуемого, например,
так:
Файл: Cargo.toml
Даже если вы выбрали уникальное имя, когда вы запустите cargo publish чтобы опубликовать крейт, вы получите предупреждение, а затем ошибку:
Это ошибка, потому что вам не хватает важной информации: необходимы описание и лицензия, чтобы люди знали, что делает ваш крейт и на каких условиях они могут его использовать. В поле Cargo.toml добавьте описание, состоящее из одного-двух предложений, поскольку оно будет появляться вместе с вашим крейтом в результатах поиска. Для поля license нужно указать значение идентификатора лицензии. В
Linux
Foundation's Software Package Data Exchange (SPDX)
перечислены идентификаторы,
которые можно использовать для этого значения. Например, чтобы указать, что вы лицензировали свой crate, используя лицензию MIT, добавьте идентификатор
MIT
:
Файл: Cargo.toml
[package]
name =
"guessing_game"
$
cargo publish
Updating crates.io index warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip-- error: failed to publish to registry at https://crates.io
Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust- lang.org/cargo/reference/manifest.html for how to upload metadata
[package]
name =
"guessing_game"
license =
"MIT"
Если вы хотите использовать лицензию, которая отсутствует в SPDX, вам нужно поместить текст этой лицензии в файл, включите файл в свой проект, а затем используйте license-file
, чтобы указать имя этого файла вместо использования ключа license
Руководство по выбору лицензии для вашего проекта выходит за рамки этой книги.
Многие люди в сообществе Rust лицензируют свои проекты так же, как и Rust, используя двойную лицензию
MIT OR Apache 2.0
. Эта практика демонстрирует, что вы также можете указать несколько идентификаторов лицензий, разделённых
OR
, чтобы иметь несколько лицензий для вашего проекта.
С добавлением уникального имени, версии, вашего описания и лицензии, файл
Cargo.toml для проекта, который готов к публикации может выглядеть следующим образом:
Файл: Cargo.toml
Документация Cargo описывает другие метаданные, которые вы можете указать, чтобы другие могли легче находить и использовать ваш крейт.
Публикация на Crates.io
Теперь, когда вы создали учётную запись, сохранили свой токен API, выбрали имя для своего крейта и указали необходимые метаданные, вы готовы к публикации! Публикация библиотеки загружает определённую версию в crates.io для использования другими.
Будьте осторожны, потому что публикация является перманентной операцией. Версия никогда не сможет быть перезаписана, а код не подлежит удалению. Одна из основных целей crates.io
- служить постоянным архивом кода, чтобы сборки всех проектов,
зависящих от crates из crates.io продолжали работать. Предоставление возможности удаления версий сделало бы выполнение этой цели невозможным. При этом количество версий крейтов, которые вы можете опубликовать, не ограничено.
Запустите команду cargo publish ещё раз. Сейчас эта команда должна выполниться успешно:
[package]
name =
"guessing_game"
version =
"0.1.0"
edition =
"2021"
description =
"A fun game where you guess what number the computer has chosen."
license =
"MIT OR Apache-2.0"
[dependencies]
Поздравляем! Теперь вы поделились своим кодом с сообществом Rust и любой может легко добавить вашу библиотеку в качестве зависимости их проекта.
Публикация новой версии существующей библиотеки
Когда вы внесли изменения в свой крейт и готовы выпустить новую версию, измените значение version
, указанное в вашем файле Cargo.toml и повторите публикацию.
Воспользуйтесь
Semantic Versioning rules
, чтобы решить, какой номер следующей версии подходит для ваших изменений. Затем запустите cargo publish
, чтобы загрузить новую версию.
Устранение устаревших версий с Crates.io с помощью cargo yank
Хотя вы не можете удалить предыдущие версии крейта, вы можете помешать любым будущим проектам добавлять его в качестве новой зависимости. Это полезно, когда версия крейта сломана по той или иной причине. В таких ситуациях Cargo поддерживает
выламывание (yanking) версии крейта.
Вычёркивание версии не позволяет новым проектам зависеть от этой версии, но при этом позволяет всем существующим проектам, зависящим от неё, продолжать работу. По сути, исключение означает, что все проекты с Cargo.lock не сломаются, а любые файлы
Cargo.lock, которые будут генерироваться в будущем, не смогут использовать исключённую версию.
Чтобы вычеркнуть версию крейта, в директории крейта, который вы опубликовали ранее, выполните команду cargo yank и укажите, какую версию вы хотите вычеркнуть.
Например, если мы опубликовали крейт под названием guessing_game версии 1.0.1 и хотим вычеркнуть её, в каталоге проекта для guessing_game мы выполним:
Добавив в команду
--undo
, вы также можете отменить выламывание и разрешить проектам начать зависеть от версии снова:
$
cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
$
cargo yank --vers 1.0.1
Updating crates.io index
Yank guessing_game:1.0.1
Вычёркивание не удаляет код. Оно не может, например, удалить случайно загруженные пароли. Если это произойдёт, вы должны немедленно сбросить эти пароли.
$
cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank guessing_game_:1.0.1
Рабочие пространства Cargo
В главе 12 мы создали пакет, который включал в себя бинарный и библиотечный крейты.
По мере развития вашего проекта может возникнуть ситуация, когда библиотечный крейт будет становиться все больше, и вы захотите разделить ваш пакет на несколько библиотечных крейтов. Cargo предоставляет функциональность под названием
workspaces, которая помогает управлять несколькими взаимосвязанными пакетами,
которые разрабатываются в тандеме.
Создание рабочего пространства
Workspace - это набор пакетов, которые используют один и тот же Cargo.lock и директорию для хранения результатов компиляции. Давайте создадим проект с использованием
workspace - мы будем использовать тривиальный код, чтобы сосредоточиться на структуре рабочего пространства. Существует несколько способов структурировать рабочую область, но мы покажем только один из них. У нас будет рабочая область,
содержащая двоичный файл и две библиотеки. Двоичный файл, который обеспечивает основную функциональность, будет зависеть от двух библиотек. Одна библиотека предоставит функцию add_one
, а вторая - add_two
. Эти три крейта будут частью одного
workspace. Начнём с создания каталога для рабочего окружения:
Далее в каталоге add мы создадим файл Cargo.toml, который будет определять конфигурацию всего рабочего окружения. В этом файле не будет секции
[package]
Вместо этого он будет начинаться с секции
[workspace]
, которая позволит нам добавить модули в рабочее пространство, указав путь к пакету с нашим бинарным крейтом; в данном случае этот путь - adder:
Файл: Cargo.toml
Затем мы создадим исполняемый крейт adder
, запустив команду cargo new в каталоге
add:
На этом этапе мы можем создать рабочее пространство, запустив команду cargo build
Файлы в каталоге add должны выглядеть следующим образом:
$
mkdir add
$
cd add
[workspace]
members = [
"adder"
,
]
$
cargo new adder
Created binary (application) `adder` package
Рабочая область содержит на верхнем уровне один каталог target, в который будут помещены скомпилированные артефакты; пакет adder не имеет собственного каталога
target. Даже если мы запустим cargo build из каталога adder, скомпилированные артефакты все равно окажутся в add/target, а не в add/adder/target. Cargo так определил директорию target в рабочем пространстве, потому что крейты в рабочем пространстве должны зависеть друг от друга. Если бы каждый крейт имел свой собственный каталог
target, каждому крейту пришлось бы перекомпилировать каждый из других крейтов в рабочем пространстве, чтобы поместить артефакты в свой собственный каталог target.
Благодаря совместному использованию единого каталога target крейты могут избежать ненужной перекомпиляции.
Добавление второго крейта в рабочее пространство
Далее давайте создадим ещё одного участника пакета в рабочей области и назовём его add_one
. Внесите изменения в Cargo.toml верхнего уровня так, чтобы указать путь
add_one в списке members
:
Файл: Cargo.toml
Затем сгенерируйте новый крейт библиотеки с именем add_one
:
Ваш каталог add должен теперь иметь следующие каталоги и файлы:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
[workspace]
members = [
"adder"
,
"add_one"
,
]
$
cargo new add_one --lib
Created library `add_one` package
В файле add_one/src/lib.rs добавим функцию add_one
:
Файл: add_one/src/lib.rs
Теперь мы можем сделать так, чтобы пакет adder с нашим исполняемым файлом зависел от пакета add_one
, содержащего нашу библиотеку. Сначала нам нужно добавить зависимость пути от add_one в adder/Cargo.toml.
Файл: adder/Cargo.toml
Cargo не исходит из того, что крейты в рабочем пространстве могут зависеть друг от друга, поэтому нам необходимо явно указать отношения зависимости.
Далее, давайте используем функцию add_one
(из крейта add_one
) в крейте adder
Откройте файл adder/src/main.rs и добавьте строку use в верхней части, чтобы ввести в область видимости новый библиотечный крейт add_one
. Затем измените функцию main для вызова функции add_one
, как показано в листинге 14-7.
Файл: adder/src/main.rs
1 ... 33 34 35 36 37 38 39 40 ... 62
Листинг 14-7: Использование функционала библиотечного крейта
add-one
в крейте
adder
Давайте соберём рабочее пространство, запустив команду cargo build в каталоге верхнего уровня add!
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target pub fn add_one
(x: i32
) -> i32
{ x +
1
}
[dependencies]
add_one = { path =
"../add_one"
} use add_one; fn main
() { let num =
10
; println!
(
"Hello, world! {num} plus one is {}!"
, add_one::add_one(num));
}
Чтобы запустить бинарный крейт из каталога add, нам нужно указать какой пакет из рабочей области мы хотим использовать с помощью аргумента
-p и названия пакета в команде cargo run
:
Запуск кода из adder/src/main.rs, который зависит от add_one
Зависимость от внешних крейтов в рабочем пространстве
Обратите внимание, что рабочая область имеет один единственный файл Cargo.lock на верхнем уровне, а не содержит Cargo.lock в каталоге каждого крейта. Это гарантирует, что все крейты используют одну и ту же версию всех зависимостей. Если мы добавим пакет rand в файлы adder/Cargo.toml и add_one/Cargo.toml, Cargo сведёт их оба к одной версии rand и запишет её в один Cargo.lock. Если заставить все крейты в рабочей области использовать одни и те же зависимости, то это будет означать, что крейты всегда будут совместимы друг с другом. Давайте добавим крейт rand в раздел
[dependencies]
в файле add_one/Cargo.toml, чтобы мы могли использовать крейт rand в крейте add_one
:
Файл: add_one/Cargo.toml
Теперь мы можем добавить use rand;
в файл add_one/src/lib.rs и сделать сборку рабочего пространства, запустив cargo build в каталоге add, что загрузит и скомпилирует rand крейт:
$
cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
$
cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
[dependencies]
rand =
"0.8.3"
Файл Cargo.lock верхнего уровня теперь содержит информацию о зависимости add_one к
крейту rand
. Тем не менее, не смотря на то что rand использован где-то в рабочем пространстве, мы не можем использовать его в других крейтах рабочего пространства,
пока не добавим крейт rand в отдельные Cargo.toml файлы. Например, если мы добавим use rand;
в файл adder/src/main.rs крейта adder
, то получим ошибку:
Чтобы исправить это, отредактируйте файл Cargo.toml для пакета adder и укажите, что rand также является его зависимостью. При сборке пакета adder rand будет добавлен в список зависимостей для adder в Cargo.lock, но никаких дополнительных копий rand загружено не будет. Cargo позаботился о том, чтобы все крейты во всех пакетах рабочей области, использующих пакет rand
, использовали одну и ту же версию, экономя нам место и гарантируя, что все крейты в рабочей области будут совместимы друг с другом.
Добавление теста в рабочее пространство
В качестве ещё одного улучшения давайте добавим тест функции add_one::add_one в add_one
:
Файл: add_one/src/lib.rs
$
cargo build
Updating crates.io index
Downloaded rand v0.8.3
--snip--
Compiling rand v0.8.3
Compiling add_one v0.1.0 (file:///projects/add/add_one) warning: unused import: `rand`
-->
add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default warning: 1 warning emitted
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
$
cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder) error[E0432]: unresolved import `rand`
-->
adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
Теперь запустите cargo test в каталоге верхнего уровня add. Запуск cargo test в
рабочем пространстве, структурированном подобно этому, запустит тесты для всех крейтов в рабочем пространстве:
Первая секция вывода показывает, что тест it_works в крейте add_one прошёл.
Следующая секция показывает, что в крейте adder не было обнаружено ни одного теста,
а последняя секция показывает, что в крейте add_one не было найдено ни одного теста документации.
Мы также можем запустить тесты для одного конкретного крейта в рабочем пространстве из каталог верхнего уровня с помощью флага
-p и указанием имени крейта для которого мы хотим запустить тесты:
pub fn add_one
(x: i32
) -> i32
{ x +
1
}
#[cfg(test)]
mod tests { use super::*;
#[test]
fn it_works
() { assert_eq!
(
3
, add_one(
2
));
}
}
$
cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.27s
Running target/debug/deps/add_one-f0253159197f7841 running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running target/debug/deps/adder-49979ff40686fa8e running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Эти выходные данные показывают, что выполнение cargo test запускает только тесты для крейта add-one и не запускает тесты крейта adder
Если вы соберётесь опубликовать крейты из рабочего пространства на crates.io
, каждый крейт будет необходимо будет опубликовать отдельно. Подобно cargo test
, мы можем опубликовать конкретный крейт из нашей рабочей области, используя флаг
-p и указав имя крейта, который мы хотим опубликовать.
Для дополнительной практики добавьте крейт add_two в данное рабочее пространство аналогичным способом, как делали с крейт add_one
!
По мере роста проекта рассмотрите возможность использования рабочих областей:
легче понять небольшие, отдельные компоненты, чем один большой кусок кода. Кроме того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются параллельно.
$
cargo test
-p add_one
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running target/debug/deps/add_one-b3235fea9a156f74 running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Установка двоичных файлов с помощью cargo install
Команда cargo install позволяет локально устанавливать и использовать исполняемые крейты. Она не предназначена для замены системных пакетов; она используется как удобный способ Rust разработчикам устанавливать инструменты,
которыми другие разработчики поделились на сайте crates.io
. Заметьте, можно устанавливать только пакеты, имеющие исполняемые целевые крейты. Исполняемой
целью (binary target) является запускаемая программа, созданная и имеющая в составе крейта файл src/main.rs или другой файл, указанный как исполняемый, в отличии от библиотечных крейтов, которые не могут запускаться сами по себе, но подходят для включения в другие программы. Обычно крейт содержит информацию в файле README,
является ли он библиотекой, исполняемым файлом или обоими вместе.
Все исполняемые файлы установленные командой cargo install сохранены в корневой установочной папке bin. Если вы установили Rust с помощью rustup.rs и у вас его нет в пользовательских конфигурациях, то этим каталогом будет $HOME/.cargo/bin. Он гарантирует, что каталог находится в вашем окружении
$PATH
, чтобы вы имели возможность запускать программы, которые вы установили командой cargo install
Так, например, в главе 12 мы упоминали, что для поиска файлов существует реализация утилиты grep на Rust под названием ripgrep
. Чтобы установить ripgrep
, мы можем выполнить следующее:
Последняя строка вывода показывает местоположение и название установленного исполняемого файла, который в случае ripgrep называется rg
. Если вашей установочной директорией является
$PATH
, как уже упоминалось ранее, вы можете запустить rg --help и начать использовать более быстрый и грубый инструмент для поиска файлов!
$
cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v11.0.2
Downloaded 1 crate (243.3 KB) in 0.88s
Installing ripgrep v11.0.2
--snip--
Compiling ripgrep v11.0.2
Finished release [optimized + debuginfo] target(s) in 3m 10s
Installing /.cargo/bin/rg
Installed package `ripgrep v11.0.2` (executable `rg`)
Расширение Cargo пользовательскими командами
Cargo спроектирован так, что вы можете расширять его новыми субкомандами без необходимости изменения самого Cargo. Если исполняемый файл доступен через переменную окружения
$PATH
и назван по шаблону cargo-something
, то его можно запускать как субкоманду Cargo cargo something
. Пользовательские команды подобные этой также перечисляются в списке доступных через cargo --list
. Возможность использовать cargo install для установки расширений и затем запускать их так же, как встроенные в Cargo инструменты, это очень удобное следствие продуманного дизайна
Cargo!
Итоги
Совместное использование кода с Cargo и crates.io является частью того, что делает экосистему Rust полезной для множества различных задач. Стандартная библиотека Rust небольшая и стабильная, но крейты легко распространять, использовать и улучшать независимо от самого языка. Не стесняйтесь делиться кодом, который был вам полезен,
через crates.io
; скорее всего, он будет полезен и кому-то ещё!
Умные указатели
Указатель — это общая концепция для переменной, которая содержит адрес участка памяти. Этот адрес «относится к», или «указывает на» некоторые другие данные.
Наиболее общая разновидность указателя в Rust — это ссылка, о которой вы узнали из главы 4. Ссылки обозначаются символом
&
и заимствуют значение, на которое указывают. Они не имеют каких-либо специальных возможностей, кроме как ссылаться на данные, и не имеют никаких накладных расходов.
Умные указатели, с другой стороны, являются структурами данных, которые не только действуют как указатель, но также имеют дополнительные метаданные и возможности.
Концепция умных указателей не уникальна для Rust: умные указатели возникли в C++ и существуют в других языках. В Rust есть разные умные указатели, определённые в стандартной библиотеке, которые обеспечивают функциональность, выходящую за рамки ссылок. Одним из примеров, который мы рассмотрим в этой главе, является тип умного указателя reference counting (подсчёт ссылок). Этот указатель позволяет иметь несколько владельцев с помощью отслеживания количества владельцев и, когда владельцев не остаётся, очищает данные.
Rust с его концепцией владения и заимствования имеет дополнительное различие между ссылками и умными указателями: в то время, как ссылки только заимствуют данные, умные указатели часто владеют данными, на которые указывают.
Ранее мы уже сталкивались с умными указателями в этой книге, хотя и не называли их так, например
String и
Vec
в главе 8. Оба этих типа считаются умными указателями,
потому что они владеют некоторой областью памяти и позволяют ею манипулировать. У
них также есть метаданные и дополнительные возможности или гарантии.
String
,
например, хранит свой размер в виде метаданных и гарантирует, что содержимое строки всегда будет в кодировке UTF-8.
Умные указатели обычно реализуются с помощью структур. Характерной чертой, которая отличает умный указатель от обычной структуры, является то, что для умных указателей реализованы типажи
Deref и
Drop
. Типаж
Deref позволяет экземпляру умного указателя вести себя как ссылка, так что вы можете написать код, работающий с ним как со ссылкой, так и как с умным указателем. Типаж
Drop позволяет написать код, который будет запускаться когда экземпляр умного указателя выйдет из области видимости. В
этой главе мы обсудим оба типажа и продемонстрируем, почему они важны для умных указателей.
Учитывая, что паттерн умного указателя является общим паттерном проектирования,
часто используемым в Rust, эта глава не описывает все существующие умные указатели.
Множество библиотек имеют свои умные указатели, и вы также можете написать свои.
Мы охватим наиболее распространённые умные указатели из стандартной библиотеки:
Box
для распределения значений в куче (памяти)
Rc
тип счётчика ссылок, который допускает множественное владение
Типы
Ref
и
RefMut
, доступ к которым осуществляется через тип
RefCell
,
который обеспечивает правила заимствования во время выполнения вместо времени компиляции
Дополнительно мы рассмотрим паттерн внутренней изменчивости (interior mutability), где неизменяемый тип предоставляет API для изменения своего внутреннего значения. Мы также обсудим ссылочные зацикленности (reference cycles): как они могут приводить к утечке памяти и как это предотвратить.
Приступим!
Использование Box для ссылки на данные в куче
Наиболее простой умный указатель - это box, чей тип записывается как
Box
. Такие переменные позволяют хранить данные в куче, а не в стеке. То, что остаётся в стеке,
является указателем на данные в куче. Обратитесь к Главе 4, чтобы рассмотреть разницу между стеком и кучей.
У Box нет проблем с производительностью, кроме хранения данных в куче вместо стека.
Но он также и не имеет множества дополнительных возможностей. Вы будете использовать его чаще всего в следующих ситуациях:
Когда у вас есть тип, размер которого невозможно определить во время компиляции, а вы хотите использовать значение этого типа в контексте, требующем точного размера.
Когда у вас есть большой объем данных и вы хотите передать владение, но при этом быть уверенным, что данные не будут скопированы
Когда вы хотите получить значение во владение и вас интересует только то, что оно относится к типу, реализующему определённый трейт, а не то, является ли оно значением какого-то конкретного типа
Мы продемонстрируем первую ситуацию в разделе "Реализация рекурсивных типов с помощью Box"
. Во втором случае, передача владения на большой объем данных может занять много времени, потому что данные копируются через стек. Для повышения производительности в этой ситуации, мы можем хранить большое количество данных в куче с помощью Box. Затем только небольшое количество данных указателя копируется в стеке, в то время как данные, на которые он ссылается, остаются в одном месте кучи.
Третий случай известен как типаж объект (trait object) и глава 17 посвящает целый раздел "Использование типаж объектов, которые допускают значения разных типов"
только этой теме. Итак, то, что вы узнаете здесь, вы примените снова в Главе 17!
Использование Box для хранения данных в куче
Прежде чем мы обсудим этот вариант использования
Box
, мы рассмотрим синтаксис и то, как взаимодействовать со значениями, хранящимися в
Box
В листинге 15-1 показано, как использовать поле для хранения значения i32
в куче:
Файл: src/main.rs
Листинг 15-1: Сохранение значения
i32
в куче с использованием box
fn main
() { let b =
Box
::new(
5
); println!
(
"b = {}"
, b);
}
Мы объявляем переменную b
со значением
Box
, указывающим на число
5
,
размещённое в куче. Эта программа выведет b = 5
; в этом случае мы получаем доступ к данным в box так же, как если бы эти данные находились в стеке. Как и любое другое значение, когда box выйдет из области видимости, как b
в конце main
, он будет удалён.
Деаллокация происходит как для box ( хранящегося в стеке), так и для данных, на которые он указывает (хранящихся в куче).
Размещать одиночные значения в куче не слишком целесообразно, поэтому вряд ли вы будете часто использовать box'ы таким образом. В большинстве ситуаций более уместно размещать такие значения, как i32
, в стеке, где они и сохраняются по умолчанию.
Давайте рассмотрим ситуацию, когда box позволяет нам определить типы, которые мы не могли бы иметь, если бы у нас не было box.
Включение рекурсивных типов с помощью Boxes
Значение рекурсивного типа может иметь другое значение такого же типа как свой компонент. Рекурсивные типы представляют собой проблему, поскольку во время компиляции Rust должен знать, сколько места занимает тип. Однако вложенность значений рекурсивных типов теоретически может продолжаться бесконечно, поэтому
Rust не может определить, сколько места потребуется. Поскольку box имеет известный размер, мы можем включить рекурсивные типы, добавив box в определение рекурсивного типа.
В качестве примера рекурсивного типа рассмотрим cons list. Это тип данных, часто встречающийся в функциональных языках программирования. Тип cons list, который мы определим, достаточно прост, за исключением наличия рекурсии; поэтому концепции,
заложенные в примере, с которым мы будем работать, пригодятся вам в любой более сложной ситуации, связанной с рекурсивными типами.
Больше информации о cons списке
cons list - это структура данных из языка программирования Lisp и его диалектов,
представляющая собой набор вложенных пар и являющаяся Lisp-версией связного списка. Его название происходит от функции cons
(сокращение от "construct function") в
Lisp, которая формирует пару из двух своих аргументов. Вызывая cons для пары, которая состоит из некоторого значения и другой пары, мы можем конструировать списки cons,
состоящие из рекурсивных пар.
Вот, пример cons list написанный на псевдокоде, содержащий список 1, 2, 3 где каждая пара заключена в круглые скобки:
(1, (2, (3, Nil)))
Каждый элемент в cons списке содержит два элемента: значение текущего элемента и следующий элемент. Последний элемент в списке содержит только значение называемое
Nil без следующего элемента. Cons список создаётся путём рекурсивного вызова функции cons
. Каноничное имя для обозначения базового случая рекурсии -
Nil
Обратите внимание, что это не то же самое, что понятие “null” или “nil” из главы 6,
которая является недействительным или отсутствующим значением.
Сons list не является часто используемой структурой данных в Rust. В большинстве случаев, когда вам нужен список элементов при использовании Rust, лучше использовать
Vec
. Другие, более сложные рекурсивные типы данных полезны в определённых ситуациях, но благодаря тому, что в этой главе мы начнём с cons list, мы сможем выяснить, как box позволяет нам определить рекурсивный тип данных без особого напряжения.
Листинг 15-2 содержит объявление перечисления cons списка. Обратите внимание, что этот код не будет компилироваться, потому что тип
List не имеет известного размера,
что мы и продемонстрируем.
Файл: src/main.rs
1 ... 34 35 36 37 38 39 40 41 ... 62
Чтобы запустить бинарный крейт из каталога add, нам нужно указать какой пакет из рабочей области мы хотим использовать с помощью аргумента
-p и названия пакета в команде cargo run
:
Запуск кода из adder/src/main.rs, который зависит от add_one
Зависимость от внешних крейтов в рабочем пространстве
Обратите внимание, что рабочая область имеет один единственный файл Cargo.lock на верхнем уровне, а не содержит Cargo.lock в каталоге каждого крейта. Это гарантирует, что все крейты используют одну и ту же версию всех зависимостей. Если мы добавим пакет rand в файлы adder/Cargo.toml и add_one/Cargo.toml, Cargo сведёт их оба к одной версии rand и запишет её в один Cargo.lock. Если заставить все крейты в рабочей области использовать одни и те же зависимости, то это будет означать, что крейты всегда будут совместимы друг с другом. Давайте добавим крейт rand в раздел
[dependencies]
в файле add_one/Cargo.toml, чтобы мы могли использовать крейт rand в крейте add_one
:
Файл: add_one/Cargo.toml
Теперь мы можем добавить use rand;
в файл add_one/src/lib.rs и сделать сборку рабочего пространства, запустив cargo build в каталоге add, что загрузит и скомпилирует rand крейт:
$
cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
$
cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
[dependencies]
rand =
"0.8.3"
Файл Cargo.lock верхнего уровня теперь содержит информацию о зависимости add_one к
крейту rand
. Тем не менее, не смотря на то что rand использован где-то в рабочем пространстве, мы не можем использовать его в других крейтах рабочего пространства,
пока не добавим крейт rand в отдельные Cargo.toml файлы. Например, если мы добавим use rand;
в файл adder/src/main.rs крейта adder
, то получим ошибку:
Чтобы исправить это, отредактируйте файл Cargo.toml для пакета adder и укажите, что rand также является его зависимостью. При сборке пакета adder rand будет добавлен в список зависимостей для adder в Cargo.lock, но никаких дополнительных копий rand загружено не будет. Cargo позаботился о том, чтобы все крейты во всех пакетах рабочей области, использующих пакет rand
, использовали одну и ту же версию, экономя нам место и гарантируя, что все крейты в рабочей области будут совместимы друг с другом.
Добавление теста в рабочее пространство
В качестве ещё одного улучшения давайте добавим тест функции add_one::add_one в add_one
:
Файл: add_one/src/lib.rs
$
cargo build
Updating crates.io index
Downloaded rand v0.8.3
--snip--
Compiling rand v0.8.3
Compiling add_one v0.1.0 (file:///projects/add/add_one) warning: unused import: `rand`
-->
add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default warning: 1 warning emitted
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
$
cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder) error[E0432]: unresolved import `rand`
-->
adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
Теперь запустите cargo test в каталоге верхнего уровня add. Запуск cargo test в
рабочем пространстве, структурированном подобно этому, запустит тесты для всех крейтов в рабочем пространстве:
Первая секция вывода показывает, что тест it_works в крейте add_one прошёл.
Следующая секция показывает, что в крейте adder не было обнаружено ни одного теста,
а последняя секция показывает, что в крейте add_one не было найдено ни одного теста документации.
Мы также можем запустить тесты для одного конкретного крейта в рабочем пространстве из каталог верхнего уровня с помощью флага
-p и указанием имени крейта для которого мы хотим запустить тесты:
pub fn add_one
(x: i32
) -> i32
{ x +
1
}
#[cfg(test)]
mod tests { use super::*;
#[test]
fn it_works
() { assert_eq!
(
3
, add_one(
2
));
}
}
$
cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.27s
Running target/debug/deps/add_one-f0253159197f7841 running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running target/debug/deps/adder-49979ff40686fa8e running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Эти выходные данные показывают, что выполнение cargo test запускает только тесты для крейта add-one и не запускает тесты крейта adder
Если вы соберётесь опубликовать крейты из рабочего пространства на crates.io
, каждый крейт будет необходимо будет опубликовать отдельно. Подобно cargo test
, мы можем опубликовать конкретный крейт из нашей рабочей области, используя флаг
-p и указав имя крейта, который мы хотим опубликовать.
Для дополнительной практики добавьте крейт add_two в данное рабочее пространство аналогичным способом, как делали с крейт add_one
!
По мере роста проекта рассмотрите возможность использования рабочих областей:
легче понять небольшие, отдельные компоненты, чем один большой кусок кода. Кроме того, хранение крейтов в рабочем пространстве может облегчить координацию между крейтами, если они часто изменяются параллельно.
$
cargo test
-p add_one
Finished test [unoptimized + debuginfo] target(s) in 0.00s
Running target/debug/deps/add_one-b3235fea9a156f74 running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Установка двоичных файлов с помощью cargo install
Команда cargo install позволяет локально устанавливать и использовать исполняемые крейты. Она не предназначена для замены системных пакетов; она используется как удобный способ Rust разработчикам устанавливать инструменты,
которыми другие разработчики поделились на сайте crates.io
. Заметьте, можно устанавливать только пакеты, имеющие исполняемые целевые крейты. Исполняемой
целью (binary target) является запускаемая программа, созданная и имеющая в составе крейта файл src/main.rs или другой файл, указанный как исполняемый, в отличии от библиотечных крейтов, которые не могут запускаться сами по себе, но подходят для включения в другие программы. Обычно крейт содержит информацию в файле README,
является ли он библиотекой, исполняемым файлом или обоими вместе.
Все исполняемые файлы установленные командой cargo install сохранены в корневой установочной папке bin. Если вы установили Rust с помощью rustup.rs и у вас его нет в пользовательских конфигурациях, то этим каталогом будет $HOME/.cargo/bin. Он гарантирует, что каталог находится в вашем окружении
$PATH
, чтобы вы имели возможность запускать программы, которые вы установили командой cargo install
Так, например, в главе 12 мы упоминали, что для поиска файлов существует реализация утилиты grep на Rust под названием ripgrep
. Чтобы установить ripgrep
, мы можем выполнить следующее:
Последняя строка вывода показывает местоположение и название установленного исполняемого файла, который в случае ripgrep называется rg
. Если вашей установочной директорией является
$PATH
, как уже упоминалось ранее, вы можете запустить rg --help и начать использовать более быстрый и грубый инструмент для поиска файлов!
$
cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v11.0.2
Downloaded 1 crate (243.3 KB) in 0.88s
Installing ripgrep v11.0.2
--snip--
Compiling ripgrep v11.0.2
Finished release [optimized + debuginfo] target(s) in 3m 10s
Installing /.cargo/bin/rg
Installed package `ripgrep v11.0.2` (executable `rg`)
Расширение Cargo пользовательскими командами
Cargo спроектирован так, что вы можете расширять его новыми субкомандами без необходимости изменения самого Cargo. Если исполняемый файл доступен через переменную окружения
$PATH
и назван по шаблону cargo-something
, то его можно запускать как субкоманду Cargo cargo something
. Пользовательские команды подобные этой также перечисляются в списке доступных через cargo --list
. Возможность использовать cargo install для установки расширений и затем запускать их так же, как встроенные в Cargo инструменты, это очень удобное следствие продуманного дизайна
Cargo!
Итоги
Совместное использование кода с Cargo и crates.io является частью того, что делает экосистему Rust полезной для множества различных задач. Стандартная библиотека Rust небольшая и стабильная, но крейты легко распространять, использовать и улучшать независимо от самого языка. Не стесняйтесь делиться кодом, который был вам полезен,
через crates.io
; скорее всего, он будет полезен и кому-то ещё!
Умные указатели
Указатель — это общая концепция для переменной, которая содержит адрес участка памяти. Этот адрес «относится к», или «указывает на» некоторые другие данные.
Наиболее общая разновидность указателя в Rust — это ссылка, о которой вы узнали из главы 4. Ссылки обозначаются символом
&
и заимствуют значение, на которое указывают. Они не имеют каких-либо специальных возможностей, кроме как ссылаться на данные, и не имеют никаких накладных расходов.
Умные указатели, с другой стороны, являются структурами данных, которые не только действуют как указатель, но также имеют дополнительные метаданные и возможности.
Концепция умных указателей не уникальна для Rust: умные указатели возникли в C++ и существуют в других языках. В Rust есть разные умные указатели, определённые в стандартной библиотеке, которые обеспечивают функциональность, выходящую за рамки ссылок. Одним из примеров, который мы рассмотрим в этой главе, является тип умного указателя reference counting (подсчёт ссылок). Этот указатель позволяет иметь несколько владельцев с помощью отслеживания количества владельцев и, когда владельцев не остаётся, очищает данные.
Rust с его концепцией владения и заимствования имеет дополнительное различие между ссылками и умными указателями: в то время, как ссылки только заимствуют данные, умные указатели часто владеют данными, на которые указывают.
Ранее мы уже сталкивались с умными указателями в этой книге, хотя и не называли их так, например
String и
Vec
в главе 8. Оба этих типа считаются умными указателями,
потому что они владеют некоторой областью памяти и позволяют ею манипулировать. У
них также есть метаданные и дополнительные возможности или гарантии.
String
,
например, хранит свой размер в виде метаданных и гарантирует, что содержимое строки всегда будет в кодировке UTF-8.
Умные указатели обычно реализуются с помощью структур. Характерной чертой, которая отличает умный указатель от обычной структуры, является то, что для умных указателей реализованы типажи
Deref и
Drop
. Типаж
Deref позволяет экземпляру умного указателя вести себя как ссылка, так что вы можете написать код, работающий с ним как со ссылкой, так и как с умным указателем. Типаж
Drop позволяет написать код, который будет запускаться когда экземпляр умного указателя выйдет из области видимости. В
этой главе мы обсудим оба типажа и продемонстрируем, почему они важны для умных указателей.
Учитывая, что паттерн умного указателя является общим паттерном проектирования,
часто используемым в Rust, эта глава не описывает все существующие умные указатели.
Множество библиотек имеют свои умные указатели, и вы также можете написать свои.
Мы охватим наиболее распространённые умные указатели из стандартной библиотеки:
Box
для распределения значений в куче (памяти)
Rc
тип счётчика ссылок, который допускает множественное владение
Типы
Ref
и
RefMut
, доступ к которым осуществляется через тип
RefCell
,
который обеспечивает правила заимствования во время выполнения вместо времени компиляции
Дополнительно мы рассмотрим паттерн внутренней изменчивости (interior mutability), где неизменяемый тип предоставляет API для изменения своего внутреннего значения. Мы также обсудим ссылочные зацикленности (reference cycles): как они могут приводить к утечке памяти и как это предотвратить.
Приступим!
Использование Box
Наиболее простой умный указатель - это box, чей тип записывается как
Box
. Такие переменные позволяют хранить данные в куче, а не в стеке. То, что остаётся в стеке,
является указателем на данные в куче. Обратитесь к Главе 4, чтобы рассмотреть разницу между стеком и кучей.
У Box нет проблем с производительностью, кроме хранения данных в куче вместо стека.
Но он также и не имеет множества дополнительных возможностей. Вы будете использовать его чаще всего в следующих ситуациях:
Когда у вас есть тип, размер которого невозможно определить во время компиляции, а вы хотите использовать значение этого типа в контексте, требующем точного размера.
Когда у вас есть большой объем данных и вы хотите передать владение, но при этом быть уверенным, что данные не будут скопированы
Когда вы хотите получить значение во владение и вас интересует только то, что оно относится к типу, реализующему определённый трейт, а не то, является ли оно значением какого-то конкретного типа
Мы продемонстрируем первую ситуацию в разделе "Реализация рекурсивных типов с помощью Box"
. Во втором случае, передача владения на большой объем данных может занять много времени, потому что данные копируются через стек. Для повышения производительности в этой ситуации, мы можем хранить большое количество данных в куче с помощью Box. Затем только небольшое количество данных указателя копируется в стеке, в то время как данные, на которые он ссылается, остаются в одном месте кучи.
Третий случай известен как типаж объект (trait object) и глава 17 посвящает целый раздел "Использование типаж объектов, которые допускают значения разных типов"
только этой теме. Итак, то, что вы узнаете здесь, вы примените снова в Главе 17!
Использование Box
Прежде чем мы обсудим этот вариант использования
Box
, мы рассмотрим синтаксис и то, как взаимодействовать со значениями, хранящимися в
Box
В листинге 15-1 показано, как использовать поле для хранения значения i32
в куче:
Файл: src/main.rs
Листинг 15-1: Сохранение значения
i32
в куче с использованием box
fn main
() { let b =
Box
::new(
5
); println!
(
"b = {}"
, b);
}
Мы объявляем переменную b
со значением
Box
, указывающим на число
5
,
размещённое в куче. Эта программа выведет b = 5
; в этом случае мы получаем доступ к данным в box так же, как если бы эти данные находились в стеке. Как и любое другое значение, когда box выйдет из области видимости, как b
в конце main
, он будет удалён.
Деаллокация происходит как для box ( хранящегося в стеке), так и для данных, на которые он указывает (хранящихся в куче).
Размещать одиночные значения в куче не слишком целесообразно, поэтому вряд ли вы будете часто использовать box'ы таким образом. В большинстве ситуаций более уместно размещать такие значения, как i32
, в стеке, где они и сохраняются по умолчанию.
Давайте рассмотрим ситуацию, когда box позволяет нам определить типы, которые мы не могли бы иметь, если бы у нас не было box.
Включение рекурсивных типов с помощью Boxes
Значение рекурсивного типа может иметь другое значение такого же типа как свой компонент. Рекурсивные типы представляют собой проблему, поскольку во время компиляции Rust должен знать, сколько места занимает тип. Однако вложенность значений рекурсивных типов теоретически может продолжаться бесконечно, поэтому
Rust не может определить, сколько места потребуется. Поскольку box имеет известный размер, мы можем включить рекурсивные типы, добавив box в определение рекурсивного типа.
В качестве примера рекурсивного типа рассмотрим cons list. Это тип данных, часто встречающийся в функциональных языках программирования. Тип cons list, который мы определим, достаточно прост, за исключением наличия рекурсии; поэтому концепции,
заложенные в примере, с которым мы будем работать, пригодятся вам в любой более сложной ситуации, связанной с рекурсивными типами.
Больше информации о cons списке
cons list - это структура данных из языка программирования Lisp и его диалектов,
представляющая собой набор вложенных пар и являющаяся Lisp-версией связного списка. Его название происходит от функции cons
(сокращение от "construct function") в
Lisp, которая формирует пару из двух своих аргументов. Вызывая cons для пары, которая состоит из некоторого значения и другой пары, мы можем конструировать списки cons,
состоящие из рекурсивных пар.
Вот, пример cons list написанный на псевдокоде, содержащий список 1, 2, 3 где каждая пара заключена в круглые скобки:
(1, (2, (3, Nil)))
Каждый элемент в cons списке содержит два элемента: значение текущего элемента и следующий элемент. Последний элемент в списке содержит только значение называемое
Nil без следующего элемента. Cons список создаётся путём рекурсивного вызова функции cons
. Каноничное имя для обозначения базового случая рекурсии -
Nil
Обратите внимание, что это не то же самое, что понятие “null” или “nil” из главы 6,
которая является недействительным или отсутствующим значением.
Сons list не является часто используемой структурой данных в Rust. В большинстве случаев, когда вам нужен список элементов при использовании Rust, лучше использовать
Vec
. Другие, более сложные рекурсивные типы данных полезны в определённых ситуациях, но благодаря тому, что в этой главе мы начнём с cons list, мы сможем выяснить, как box позволяет нам определить рекурсивный тип данных без особого напряжения.
Листинг 15-2 содержит объявление перечисления cons списка. Обратите внимание, что этот код не будет компилироваться, потому что тип
List не имеет известного размера,
что мы и продемонстрируем.
Файл: src/main.rs
1 ... 34 35 36 37 38 39 40 41 ... 62