ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1160
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Также можно указать более одного ограничения типажа. Допустим, мы хотели бы чтобы notify использовал как форматирование вывода так и summarize для параметра item
: тогда мы указываем что в notify параметр item должен реализовывать оба типажа
Display и
Summary
. Мы можем сделать это используя синтаксис
+
:
Синтаксис
+
также допустим с ограничениями типажа для обобщённых типов:
При наличии двух ограничений типажа, тело метода notify может вызывать summarize и использовать
{}
для форматирования item при его печати.
Более ясные границы типажа с помощью where
Использование слишком большого количества ограничений типажа имеет свои недостатки. Каждый обобщённый тип имеет свои границы типажа, поэтому функции с несколькими параметрами обобщённого типа могут содержать много информации об ограничениях между названием функции и списком её параметров затрудняющих чтение сигнатуры. По этой причине в Rust есть альтернативный синтаксис для определения ограничений типажа внутри предложения where после сигнатуры функции.
Поэтому вместо того, чтобы писать так:
можно использовать where таким образом:
Сигнатура этой функции менее загромождена: название функции, список параметров, и возвращаемый тип находятся рядом, а сигнатура не содержит в себе множество ограничений типажа.
Возврат значений типа реализующего определённый типаж
Также можно использовать синтаксис impl Trait в возвращаемой позиции, чтобы вернуть значение некоторого типа реализующего типаж, как показано здесь:
pub fn notify
(item: &(
impl
Summary + Display)) { pub fn notify
, U:
Clone
+
Debug
>(t: &T, u: &U) -> i32
{ fn some_function
where
T: Display +
Clone
,
U:
Clone
+
Debug
,
{
Используя impl Summary для возвращаемого типа, мы указываем, что функция returns_summarizable возвращает некоторый тип, который реализует типаж
Summary без обозначения конкретного типа. В этом случае returns_summarizable возвращает
Tweet
,
но код, вызывающий эту функцию, этого не знает.
Возможность возвращать тип, который определяется только реализуемым им признаком, особенно полезна в контексте замыканий и итераторов, которые мы рассмотрим в Главе 13. Замыкания и итераторы создают типы, которые знает только компилятор или типы, которые очень долго указывать. Синтаксис impl Trait позволяет кратко указать, что функция возвращает некоторый тип, который реализует типаж
Iterator без необходимости писать очень длинный тип.
Однако, impl Trait возможно использовать, если возвращаете только один тип.
Например, данный код, который возвращает значения или типа
NewsArticle или типа
Tweet
, но в качестве возвращаемого типа объявляет impl Summary
, не будет работать:
fn returns_summarizable
() -> impl
Summary {
Tweet { username:
String
::from(
"horse_ebooks"
), content:
String
::from(
"of course, as you probably already know, people"
,
), reply: false
, retweet: false
,
}
} fn returns_summarizable
(switch: bool
) -> impl
Summary { if switch {
NewsArticle { headline:
String
::from(
"Penguins win the Stanley Cup Championship!"
,
), location:
String
::from(
"Pittsburgh, PA, USA"
), author:
String
::from(
"Iceburgh"
), content:
String
::from(
"The Pittsburgh Penguins once again are the best \ hockey team in the NHL."
,
),
}
} else
{
Tweet { username:
String
::from(
"horse_ebooks"
), content:
String
::from(
"of course, as you probably already know, people"
,
), reply: false
, retweet: false
,
}
}
}
Возврат либо
NewsArticle либо
Tweet не допускается из-за ограничений того, как реализован синтаксис impl Trait в компиляторе. Мы рассмотрим, как написать функцию с таким поведением в разделе "Использование объектов типажей, которые разрешены для значений или разных типов"
Главы 17.
Использование ограничений типажа для условной реализации
методов
Используя ограничение типажа с блоком impl
, который использует параметры обобщённого типа, можно реализовать методы условно, для тех типов, которые реализуют указанный типаж. Например, тип
Pair
в листинге 10-16 всегда реализует функцию new для возврата нового экземпляра
Pair
(вспомните раздел
“Определение методов”
Главы 5 где
Self является псевдонимом типа для типа блока impl
, который в данном случае является
Pair
). Но в следующем блоке impl тип
Pair
реализует метод cmp_display только если его внутренний тип
T
реализует типаж
PartialOrd
(позволяющий сравнивать) и типаж
Display
(позволяющий выводить на печать).
Файл: src/lib.rs
Листинг 10-15: Условная реализация методов у обобщённых типов в зависимости от ограничений типажа
Мы также можем условно реализовать типаж для любого типа, который реализует другой типаж. Реализации типажа для любого типа, который удовлетворяет ограничениям типажа, называются общими реализациями и широко используются в стандартной библиотеке Rust. Например, стандартная библиотека реализует типаж
ToString для use std::fmt::Display; struct
Pair
} impl
(x: T, y: T) ->
Self
{
Self
{ x, y }
}
} impl
> Pair
(&
self
) { if self
.x >= self
.y { println!
(
"The largest member is x = {}"
, self
.x);
} else
{ println!
(
"The largest member is y = {}"
, self
.y);
}
}
}
любого типа, который реализует типаж
Display
. Блок impl в стандартной библиотеке выглядит примерно так:
Поскольку стандартная библиотека имеет эту общую реализацию, то можно вызвать метод to_string определённый типажом
ToString для любого типа, который реализует типаж
Display
. Например, мы можем превратить целые числа в их соответствующие
String значения, потому что целые числа реализуют типаж
Display
:
Общие реализации приведены в документации к типажу в разделе "Implementors".
Типажи и ограничения типажей позволяют писать код, который использует параметры обобщённого типа для уменьшения дублирования кода, а также указывая компилятору,
что мы хотим обобщённый тип, чтобы иметь определённое поведение. Затем компилятор может использовать информацию про ограничения типажа, чтобы проверить, что все конкретные типы, используемые с нашим кодом, обеспечивают правильное поведение. В динамически типизированных языках мы получили бы ошибку во время выполнения, если бы вызвали метод для типа, который не реализует тип определяемый методом. Но Rust перемещает эти ошибки на время компиляции, поэтому мы вынуждены исправить проблемы, прежде чем наш код начнёт работать. Кроме того,
мы не должны писать код, который проверяет своё поведение во время выполнения,
потому что это уже проверено во время компиляции. Это повышает производительность без необходимости отказываться от гибкости обобщённых типов.
impl
ToString for
T {
// --snip--
} let s =
3
.to_string();
Display
. Блок impl в стандартной библиотеке выглядит примерно так:
Поскольку стандартная библиотека имеет эту общую реализацию, то можно вызвать метод to_string определённый типажом
ToString для любого типа, который реализует типаж
Display
. Например, мы можем превратить целые числа в их соответствующие
String значения, потому что целые числа реализуют типаж
Display
:
Общие реализации приведены в документации к типажу в разделе "Implementors".
Типажи и ограничения типажей позволяют писать код, который использует параметры обобщённого типа для уменьшения дублирования кода, а также указывая компилятору,
что мы хотим обобщённый тип, чтобы иметь определённое поведение. Затем компилятор может использовать информацию про ограничения типажа, чтобы проверить, что все конкретные типы, используемые с нашим кодом, обеспечивают правильное поведение. В динамически типизированных языках мы получили бы ошибку во время выполнения, если бы вызвали метод для типа, который не реализует тип определяемый методом. Но Rust перемещает эти ошибки на время компиляции, поэтому мы вынуждены исправить проблемы, прежде чем наш код начнёт работать. Кроме того,
мы не должны писать код, который проверяет своё поведение во время выполнения,
потому что это уже проверено во время компиляции. Это повышает производительность без необходимости отказываться от гибкости обобщённых типов.
impl
ToString for
T {
// --snip--
} let s =
3
.to_string();
Валидация ссылок при помощи времён жизни
Сроки (времена) жизни - ещё один вид обобщений, с которыми мы уже встречались. Если раньше мы использовали обобщения, чтобы убедиться, что тип обладает нужным нам поведением, теперь мы будем использовать сроки жизни для того, чтобы быть уверенными, что ссылки действительны как минимум столько времени в процессе исполнения программы, сколько нам требуется.
В разделе "Ссылки и заимствование"
главы 4, мы кое о чем умолчали: у каждой ссылки в
Rust есть своё время жизни - область кода, на протяжении которого данная ссылка действительна (valid). В большинстве случаев сроки жизни выводятся неявно - так же, как у типов (нам требуется явно объявлять типы лишь в тех случаях, когда при автоматическом выведении типа возможны варианты). Точно так же мы должны явно объявлять сроки жизни тех ссылок, для которых времена жизни могут быть определены компилятором по-разному. Rust требует от нас объявлять взаимосвязи посредством обобщённых параметров сроков жизни - чтобы убедиться в том, что во время исполнения все действующие ссылки будут корректными.
Аннотирование времени жизни — это концепция, отсутствующая в большинстве других языков программирования, так что она может показаться незнакомой. Хотя в этой главе мы не будем рассматривать времена жизни во всех деталях, тем не менее, мы обсудим основные ситуации, в которых вы можете столкнуться с синтаксисом времени жизни, что позволит вам получше ознакомиться с этой концепцией.
Времена жизни предотвращают появление "повисших" ссылок
Основное предназначение сроков жизни — предотвращать появление так называемых
"повисших ссылок" (dangling references), из-за которых программа обращается не к тем данным, к которым она собиралась обратиться. Рассмотрим программу из листинга 10-
16, имеющую внешнюю и внутреннюю области видимости.
Листинг 10-16: Попытка использования ссылки, значение которой вышло из области видимости
Примечание: примеры в листингах 10-16, 10-17 и 10-23 объявляют переменные без указания их начального значения, поэтому имя переменной существует во fn main
() { let r;
{ let x =
5
; r = &x;
} println!
(
"r: {}"
, r);
}
внешней области видимости. На первый взгляд может показаться, что это противоречит отсутствию в Rust нулевых (null) значений. Однако, если мы попытаемся использовать переменную, прежде чем присвоить ей значение, мы получим ошибку компиляции, которая показывает, что Rust действительно не разрешает нулевые (null) значения.
Внешняя область видимости объявляет переменную с именем r
без начального значения, а внутренняя область объявляет переменную с именем x
с начальным значением
5
. Во внутренней области мы пытаемся установить значение r
как ссылку на x
. Затем внутренняя область видимости заканчивается и мы пытаемся напечатать значение из r
. Этот код не будет скомпилирован, потому что значение на которое ссылается r
исчезает из области видимости, прежде чем мы попробуем использовать его. Вот сообщение об ошибке:
Переменная x
«не живёт достаточно долго». Причина в том, что x
выйдет из области видимости, когда эта внутренняя область закончится в строке 7. Но r
все ещё является действительной во внешней области видимости; поскольку её охват больше, мы говорим, что она «живёт дольше». Если бы Rust позволил такому коду работать, то переменная r
смогла бы ссылаться на память, которая уже была освобождена (в тот момент, когда x
вышла из внутренней области видимости), и всё что мы попытались бы сделать с r
работало бы неправильно. Как же Rust определяет, что этот код некорректен? Он использует для этого анализатор заимствований (borrow checker).
Внешняя область видимости объявляет переменную с именем r
без начального значения, а внутренняя область объявляет переменную с именем x
с начальным значением
5
. Во внутренней области мы пытаемся установить значение r
как ссылку на x
. Затем внутренняя область видимости заканчивается и мы пытаемся напечатать значение из r
. Этот код не будет скомпилирован, потому что значение на которое ссылается r
исчезает из области видимости, прежде чем мы попробуем использовать его. Вот сообщение об ошибке:
Переменная x
«не живёт достаточно долго». Причина в том, что x
выйдет из области видимости, когда эта внутренняя область закончится в строке 7. Но r
все ещё является действительной во внешней области видимости; поскольку её охват больше, мы говорим, что она «живёт дольше». Если бы Rust позволил такому коду работать, то переменная r
смогла бы ссылаться на память, которая уже была освобождена (в тот момент, когда x
вышла из внутренней области видимости), и всё что мы попытались бы сделать с r
работало бы неправильно. Как же Rust определяет, что этот код некорректен? Он использует для этого анализатор заимствований (borrow checker).
1 ... 20 21 22 23 24 25 26 27 ... 62
Анализатор заимствований
Компилятор Rust имеет в своём составе анализатор заимствований, который сравнивает области видимости для определения, являются ли все заимствования действительными.
В листинге 10-17 показан тот же код, что и в листинге 10-16, но с аннотациями,
показывающими времена жизни переменных.
$
cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0597]: `x` does not live long enough
-->
src/main.rs:6:13
|
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {}", r);
| - borrow later used here
For more information about this error, try `rustc --explain E0597`. error: could not compile `chapter10` due to previous error
Пример 10-17: Аннотация времён жизни переменных
r
и
x
, с помощью идентификаторов времени жизни
'a
и
'b
, соответственно
Здесь мы описали время жизни для r
с помощью 'a и время жизни x
с помощью 'b
Как видите, время жизни 'b внутреннего блока гораздо меньше, чем время жизни 'a внешнего блока. Во время компиляции Rust сравнивает продолжительность двух времён жизни и видит, что r
имеет время жизни 'a
, но ссылается на память со временем жизни 'b
. Программа отклоняется, потому что 'b короче, чем 'a
: объект ссылки не живёт так же долго, как сама ссылка.
Листинг 10-18 исправляет код, чтобы в нём не было повисшей ссылки, и компилируется без ошибок.
Листинг 10-18: Ссылка корректна, так как данные имеют более продолжительное время жизни, чем ссылка
на эти данные
Здесь переменная x
имеет время жизни 'b
, которое больше, чем время жизни 'a
. Это означает, что переменная r
может ссылаться на переменную x
потому что Rust знает,
что ссылка в переменной r
будет всегда действительной до тех пор, пока переменная x
является валидной.
После того, как мы на примерах рассмотрели времена жизни ссылок и обсудили как Rust их анализирует, давайте поговорим об обобщённых временах жизни входных параметров и возвращаемых значений функций.
Обобщённые времена жизни в функциях
fn main
() { let r;
// ---------+-- 'a
// |
{
// |
let x =
5
;
// -+-- 'b |
r = &x;
// | |
}
// -+ |
// |
println!
(
"r: {}"
, r);
// |
}
// ---------+
fn main
() { let x =
5
;
// ----------+-- 'b
// |
let r = &x;
// --+-- 'a |
// | |
println!
(
"r: {}"
, r);
// | |
// --+ |
}
// ----------+
Напишем функцию, которая возвращает более длинный из двух срезов строки. Эта функция принимает два среза строки и возвращает один срез строки. После того как мы реализовали функцию longest
, код в листинге 10-19 должен вывести
The longest string is abcd
Файл: src/main.rs
Листинг 10-19: Функция
main
вызывает функцию
longest
для поиска наибольшего из двух срезов строки
Обратите внимание, что мы хотим чтобы функция принимала строковые срезы, которые являются ссылками, а не строки, потому что мы не хотим, чтобы функция longest забирала во владение свои параметры. Обратитесь к разделу "Строковые срезы как параметры"
Главы 4 для более подробного обсуждения того, почему параметры используемые в листинге 10-19 выбраны именно таким образом.
Если мы попробуем реализовать функцию longest так, как это показано в листинге 10-
20, программа не скомпилируется:
Файл: src/main.rs
Листинг 10-20: Реализация функции
longest
, которая возвращает наибольший срез строки, но пока не
компилируется
Вместо этого мы получим следующую ошибку, говорящую о временах жизни:
fn main
() { let string1 =
String
::from(
"abcd"
); let string2 =
"xyz"
; let result = longest(string1.as_str(), string2); println!
(
"The longest string is {}"
, result);
} fn longest
(x: &
str
, y: &
str
) -> &
str
{ if x.len() > y.len() { x
} else
{ y
}
}
Текст ошибки показывает, что возвращаемому типу нужен обобщённый параметр времени жизни, потому что Rust не может определить, относится ли возвращаемая ссылка к x
или к y
. На самом деле, мы тоже не знаем, потому что блок if в теле функции возвращает ссылку на x
, а блок else возвращает ссылку на y
!
Когда мы определяем эту функцию, мы не знаем конкретных значений, которые будут в неё передаваться. Поэтому мы не знаем какая из ветвей оператора if или else будет выполнена. Мы также не знаем конкретных времён жизни ссылок, которые будут переданы в функцию, поэтому мы не можем посмотреть на их области видимости, как мы делали в примерах 10-17 и 10-18, чтобы определить, будет ли возвращаемая нами ссылка корректной во всех случаях. Анализатор заимствований также не может этого определить, потому что он не знает как времена жизни переменных x
и y
соотносятся с временем жизни возвращаемого значения. Чтобы исправить эту ошибку, мы добавим обобщённый параметр времени жизни, который определит отношения между ссылками таким образом, чтобы анализатор заимствований мог провести свой анализ.
Синтаксис аннотации времени жизни
Аннотации времени жизни не меняют срок, как долго живёт та или иная ссылка. Они скорее описывают, как соотносятся между собой времена жизни нескольких ссылок, не влияя на само время жизни. Точно так же, как функции могут принимать любой тип,
когда в сигнатуре указан параметр обобщённого типа, функции могут принимать ссылки с любым временем жизни, указанным с помощью параметра обобщённого времени жизни.
Аннотации времени жизни имеют немного необычный синтаксис: имена параметров времени жизни должны начинаться с апострофа (
'
), пишутся маленькими буквами, и обычно очень короткие, как и имена обобщённых типов. Большинство людей использует имя 'a в качестве первой аннотации времени жизни. Аннотации
$
cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10) error[E0106]: missing lifetime specifier
-->
src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`. error: could not compile `chapter10` due to previous error