ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1118
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Использование структур для
структурирования связанных данных
Структура (struct) — это пользовательский тип данных, позволяющий назвать и упаковать вместе несколько связанных значений, составляющих значимую логическую группу. Если вы знакомы с объектно-ориентированными языками, структура похожа на атрибуты данных объекта. В этой главе мы сравним и сопоставим кортежи со структурами, чтобы опираться на то, что вы уже знаете, и продемонстрируем, когда структуры являются лучшим способом группировки данных.
Мы продемонстрируем, как определять структуры и создавать их экземпляры. Мы обсудим, как определить ассоциированные функции, особенно ассоциированные функции, называемые методами, для указания поведения, ассоциированного с типом структуры. Структуры и перечисления (обсуждаемые в главе 6) являются строительными блоками для создания новых типов в предметной области вашей программы. Они дают возможность в полной мере воспользоваться преимуществами проверки типов во время компиляции Rust.
1 ... 5 6 7 8 9 10 11 12 ... 62
Определение и инициализация структур
Структуры похожи на кортежи, рассмотренные в разделе "Кортежи"
, так как оба хранят несколько связанных значений. Как и кортежи, части структур могут быть разных типов.
В отличие от кортежей, в структуре необходимо именовать каждую часть данных для понимания смысла значений. Добавление этих имён обеспечивает большую гибкость структур по сравнению с кортежами: не нужно полагаться на порядок данных для указания значений экземпляра или доступа к ним.
Для определения структуры указывается ключевое слово struct и её название.
Название должно описывать значение частей данных, сгруппированных вместе. Далее, в фигурных скобках для каждой новой части данных поочерёдно определяются имя части данных и её тип. Каждая пара имя: тип называется полем. Листинг 5-1 описывает структуру для хранения информации об учётной записи пользователя:
Листинг 5-1: Определение структуры
User
После определения структуры можно создавать её экземпляр, назначая определённое значение каждому полю с соответствующим типом данных. Чтобы создать экземпляр, мы указываем имя структуры, затем добавляем фигурные скобки и включаем в них пары ключ: значение
(key: value), где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании структуры (но всё-таки желательно для удобства чтения). Другими словами, объявление структуры - это как шаблон нашего типа, в то время как экземпляр структуры использует этот шаблон, заполняя его определёнными данными, для создания значений нашего типа. Например, можно объявить пользователя как в листинге 5-2:
Листинг 5-2: Создание экземпляра структуры
User
Чтобы получить конкретное значение из структуры, мы используем запись через точку.
Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы struct
User
{ active: bool
, username:
String
, email:
String
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
};
}
используем user1.email
. Если экземпляр является изменяемым, мы можем поменять значение, используя точечную нотацию и присвоение к конкретному полю. В Листинге
5-3 показано, как изменить значение в поле email изменяемого экземпляра
User
Листинг 5-3: Изменение значения в поле
email
экземпляра
User
Заметим, что весь экземпляр структуры должен быть изменяемым; Rust не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания структуры в качестве последнего выражения тела функции для неявного возврата нового экземпляра.
На листинге 5-4 функция build_user возвращает экземпляр
User с указанным адресом и именем. Поле active получает значение true
, а поле sign_in_count получает значение
1
Листинг 5-4: Функция
build_user
, которая принимает email и имя пользователя и возвращает экземпляр
User
Имеет смысл называть параметры функции теми же именами, что и поля структуры, но необходимость повторять email и username для названий полей и переменных несколько утомительна. Если структура имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!
Использование сокращённой инициализации поля
Так как имена входных параметров функции и полей структуры являются полностью идентичными в листинге 5-4, возможно использовать синтаксис сокращённой
fn main
() { let mut user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
}; user1.email =
String
::from(
"anotheremail@example.com"
);
} fn build_user
(email:
String
, username:
String
) -> User {
User { email: email, username: username, active: true
, sign_in_count:
1
,
}
}
инициализации поля, чтобы переписать build_user так, чтобы он работал точно также,
но не содержал повторений для email и username
, как в листинге 5-5.
Листинг 5-5: Функция
build_user
, использующая сокращённую инициализацию поля, когда параметры
email
и
username
имеют те же имена, что и поля struct
Здесь происходит создание нового экземпляра структуры
User
, которая имеет поле с именем email
. Мы хотим установить поле структуры email значением входного параметра email функции build_user
. Так как поле email и входной параметр функции email имеют одинаковое название, можно писать просто email вместо кода email: email
Создание экземпляра структуры из экземпляра другой структуры с
помощью синтаксиса обновления структуры
Часто бывает полезно создать новый экземпляр структуры, который включает большинство значений из другого экземпляра, но некоторые из них изменяет. Это можно сделать с помощью синтаксиса обновления структуры.
Сначала в листинге 5-6 показано, как обычно создаётся новый экземпляр
User в user2
без синтаксиса обновления. Мы задаём новое значение для email
, но в остальном используем те же значения из user1
, которые были заданы в листинге 5-2.
Листинг 5-6: Создание нового экземпляра
User
с использованием некоторых значений из экземпляра
user1
Используя синтаксис обновления структуры, можно получить тот же эффект, используя меньше кода как показано в листинге 5-7. Синтаксис указывает, что оставшиеся поля fn build_user
(email:
String
, username:
String
) -> User {
User { email, username, active: true
, sign_in_count:
1
,
}
} fn main
() {
// --snip-- let user2 = User { active: user1.active, username: user1.username, email:
String
::from(
"another@example.com"
), sign_in_count: user1.sign_in_count,
};
}
устанавливаются неявно и должны иметь значения из указанного экземпляра.
Листинг 5-7: Использование синтаксиса обновления структуры для установки нового значения
email
для
экземпляра
User
, но использование остальных значений из экземпляра
user1
Код в листинге 5-7 также создаёт экземпляр в user2
, который имеет другое значение для email
, но с тем же значением для полей username
, active и sign_in_count из user1
Оператор
..user1
должен стоять последним для указания на получение значений всех оставшихся полей из соответствующих полей в user1
, но можно указать значения для любого количества полей в любом порядке, независимо от порядка полей в определении структуры.
Заметим, что синтаксис обновления структуры использует
=
как присваивание. Это связано с перемещением данных, как мы видели в разделе "Способы взаимодействия переменных и данных: перемещение"
. В этом примере мы больше не можем использовать user1
после создания user2
, потому что
String в поле username из user1
было перемещено в user2
. Если бы мы задали user2
новые значения
String для email и username
, и при этом использовать только значения active и sign_in_count из user1
, то user1
все ещё будет действительным после создания user2
. Типы active и sign_in_count являются типами, реализующими типаж
Copy
, поэтому будет применяться поведение, о котором мы говорили в разделе "Стековые данные:
Копирование"
Кортежные структуры: структуры без именованных полей для
создания разных типов
Rust также поддерживает структуры, похожие на кортежи, которые называются
кортежные структуры. Кортежные структуры обладают дополнительным смыслом,
который даёт имя структуры, но при этом не имеют имён, связанных с их полями. Скорее,
они просто хранят типы полей. Кортежные структуры полезны, когда вы хотите дать имя всему кортежу и сделать кортеж отличным от других кортежей, и когда именование каждого поля, как в обычной структуре, было бы многословным или избыточным.
Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, за которым следуют типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами
Color и
Point
:
fn main
() {
// --snip-- let user2 = User { email:
String
::from(
"another@example.com"
),
..user1
};
}
Обратите внимание, что значения black и origin
— это разные типы, потому что они являются экземплярами разных кортежных структур. Каждая определяемая вами структура имеет собственный тип, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, принимающая параметр типа
Color
, не может принимать
Point в качестве аргумента, даже если оба типа состоят из трёх значений i32
. В остальном экземпляры кортежных структур похожи на кортежи в том смысле, что вы можете деструктурировать их на отдельные части и использовать
, за которой следует индекс для доступа к отдельному значению.
Единично-подобные структуры: структуры без полей
Также можно определять структуры, не имеющие полей! Они называются единично-
подобными структурами, поскольку ведут себя аналогично
()
, единичному типу, о котором мы говорили в разделе "Кортежи"
. Единично-подобные структуры могут быть полезны, когда требуется реализовать типаж для некоторого типа, но у вас нет данных,
которые нужно хранить в самом типе. Мы обсудим типажи в главе 10. Вот пример объявления и создание экземпляра единичной структуры с именем
AlwaysEqual
:
Чтобы определить
AlwaysEqual
, мы используем ключевое слово struct
, желаемое имя,
а затем точку с запятой. Нет необходимости в фигурных или круглых скобках! Затем мы можем получить экземпляр
AlwaysEqual в переменной subject аналогичным образом:
используя имя, которое мы определили, без фигурных и круглых скобок. Представим, что в дальнейшем мы реализуем поведение для этого типа таким образом, что каждый экземпляр
AlwaysEqual всегда будет равен каждому экземпляру любого другого типа,
возможно, с целью получения ожидаемого результата для тестирования. Для реализации такого поведения нам не нужны никакие данные! В главе 10 вы увидите, как определять черты и реализовывать их для любого типа, включая единично-подобные структуры.
Владение данными структуры
struct
Color
(
i32
, i32
, i32
); struct
Point
(
i32
, i32
, i32
); fn main
() { let black = Color(
0
,
0
,
0
); let origin = Point(
0
,
0
,
0
);
} struct
AlwaysEqual
; fn main
() { let subject = AlwaysEqual;
}
В определении структуры
User в листинге 5-1 мы использовали владеющий тип
String вместо типа строковой срез
&str
. Это осознанный выбор, поскольку мы хотим, чтобы каждый экземпляр этой структуры владел всеми своими данными и чтобы эти данные были действительны до тех пор, пока действительна вся структура.
Структуры также могут хранить ссылки на данные, принадлежащие кому-то другому,
но для этого необходимо использовать возможность Rust время жизни, которую мы обсудим в главе 10. Время жизни гарантирует, что данные, на которые ссылается структура, будут действительны до тех пор, пока существует структура. Допустим,
если попытаться сохранить ссылку в структуре без указания времени жизни, как в следующем примере; это не сработает:
Имя файла: src/main.rs
Компилятор будет жаловаться на необходимость определения времени жизни ссылок:
struct
User
{ active: bool
, username: &
str
, email: &
str
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
"someone@example.com"
, username:
"someusername123"
, active: true
, sign_in_count:
1
,
};
}
В главе 10 мы обсудим, как исправить эти ошибки, чтобы иметь возможность хранить ссылки в структурах, а пока мы исправим подобные ошибки, используя владеющие типы вроде
String вместо ссылок
&str
$
cargo run
Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier
-->
src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 username: &'a str,
| error[E0106]: missing lifetime specifier
-->
src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 | username: &str,
4 email: &'a str,
|
For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` due to 2 previous errors
Пример использования структур
Чтобы понять, когда нам может понадобиться использование структур, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.
Давайте создадим новый проект программы при помощи Cargo и назовём его rectangles.
Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Листинг 5-8 показывает один из коротких вариантов кода, который позволит нам сделать именно то, что надо, в файле проекта
src/main.rs.
Файл: src/main.rs
Листинг 5-8: вычисление площади прямоугольника, заданного отдельными переменными ширины и
высоты
Теперь запустим программу, используя cargo run
:
Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area с
каждым измерением, но мы можем улучшить его ясность и читабельность.
Проблема данного метода очевидна из сигнатуры area
:
Функция area должна вычислять площадь одного прямоугольника, но функция, которую мы написали, имеет два параметра, и нигде в нашей программе не ясно, что эти параметры взаимосвязаны. Было бы более читабельным и управляемым сгруппировать fn main
() { let width1 =
30
; let height1 =
50
; println!
(
"The area of the rectangle is {} square pixels."
, area(width1, height1)
);
} fn area
(width: u32
, height: u32
) -> u32
{ width * height
}
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels. fn area
(width: u32
, height: u32
) -> u32
{
ширину и высоту вместе. В разделе
«Кортежи»
главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.
Рефакторинг при помощи кортежей
Листинг 5-9 — это другая версия программы, использующая кортежи.
Файл: src/main.rs
Листинг 5-9: определение ширины и высоты прямоугольника с помощью кортежа
С одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.
Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже с индексом
0
, а высота height
— с индексом
1
. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.
Рефакторинг при помощи структур: добавим больше смысла
Мы используем структуры, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в листинге 5-10.
Файл: src/main.rs fn main
() { let rect1 = (
30
,
50
); println!
(
"The area of the rectangle is {} square pixels."
, area(rect1)
);
} fn area
(dimensions: (
u32
, u32
)) -> u32
{ dimensions.
0
* dimensions.
1
}
Листинг 5-10: определение структуры
Rectangle
Здесь мы определили структуру и дали ей имя
Rectangle
. Внутри фигурных скобок определили поля как width и height
, оба — типа u32
. Затем в main создали конкретный экземпляр
Rectangle с шириной в 30 и высотой в 50 единиц.
Наша функция area теперь определена с одним параметром, названным rectangle
, чей тип является неизменяемым заимствованием структуры
Rectangle
. Как упоминалось в главе 4, необходимо заимствовать структуру, а не передавать её во владение. Таким образом функция main сохраняет rect1
в собственности и может использовать её
дальше. По этой причине мы и используем
&
в сигнатуре и в месте вызова функции.
Функция area получает доступ к полям width и height экземпляра
Rectangle
(обратите внимание, что доступ к полям заимствованного экземпляра структуры не приводит к перемещению значений полей, поэтому вы часто видите заимствования структур). Наша сигнатура функции для area теперь говорит именно то, что мы имеем в виду: вычислить площадь
Rectangle
, используя его поля width и height
. Это означает, что ширина и высота связаны друг с другом, и даёт описательные имена значениям, а не использует значения индекса кортежа
0
и
1
. Это торжество ясности.
Добавление полезной функциональности при помощи выводимых
1 ... 6 7 8 9 10 11 12 13 ... 62
Определение и инициализация структур
Структуры похожи на кортежи, рассмотренные в разделе "Кортежи"
, так как оба хранят несколько связанных значений. Как и кортежи, части структур могут быть разных типов.
В отличие от кортежей, в структуре необходимо именовать каждую часть данных для понимания смысла значений. Добавление этих имён обеспечивает большую гибкость структур по сравнению с кортежами: не нужно полагаться на порядок данных для указания значений экземпляра или доступа к ним.
Для определения структуры указывается ключевое слово struct и её название.
Название должно описывать значение частей данных, сгруппированных вместе. Далее, в фигурных скобках для каждой новой части данных поочерёдно определяются имя части данных и её тип. Каждая пара имя: тип называется полем. Листинг 5-1 описывает структуру для хранения информации об учётной записи пользователя:
Листинг 5-1: Определение структуры
User
После определения структуры можно создавать её экземпляр, назначая определённое значение каждому полю с соответствующим типом данных. Чтобы создать экземпляр, мы указываем имя структуры, затем добавляем фигурные скобки и включаем в них пары ключ: значение
(key: value), где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании структуры (но всё-таки желательно для удобства чтения). Другими словами, объявление структуры - это как шаблон нашего типа, в то время как экземпляр структуры использует этот шаблон, заполняя его определёнными данными, для создания значений нашего типа. Например, можно объявить пользователя как в листинге 5-2:
Листинг 5-2: Создание экземпляра структуры
User
Чтобы получить конкретное значение из структуры, мы используем запись через точку.
Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы struct
User
{ active: bool
, username:
String
, email:
String
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
};
}
используем user1.email
. Если экземпляр является изменяемым, мы можем поменять значение, используя точечную нотацию и присвоение к конкретному полю. В Листинге
5-3 показано, как изменить значение в поле email изменяемого экземпляра
User
Листинг 5-3: Изменение значения в поле
email
экземпляра
User
Заметим, что весь экземпляр структуры должен быть изменяемым; Rust не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания структуры в качестве последнего выражения тела функции для неявного возврата нового экземпляра.
На листинге 5-4 функция build_user возвращает экземпляр
User с указанным адресом и именем. Поле active получает значение true
, а поле sign_in_count получает значение
1
Листинг 5-4: Функция
build_user
, которая принимает email и имя пользователя и возвращает экземпляр
User
Имеет смысл называть параметры функции теми же именами, что и поля структуры, но необходимость повторять email и username для названий полей и переменных несколько утомительна. Если структура имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!
Использование сокращённой инициализации поля
Так как имена входных параметров функции и полей структуры являются полностью идентичными в листинге 5-4, возможно использовать синтаксис сокращённой
fn main
() { let mut user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
}; user1.email =
String
::from(
"anotheremail@example.com"
);
} fn build_user
(email:
String
, username:
String
) -> User {
User { email: email, username: username, active: true
, sign_in_count:
1
,
}
}
инициализации поля, чтобы переписать build_user так, чтобы он работал точно также,
но не содержал повторений для email и username
, как в листинге 5-5.
Листинг 5-5: Функция
build_user
, использующая сокращённую инициализацию поля, когда параметры
email
и
username
имеют те же имена, что и поля struct
Здесь происходит создание нового экземпляра структуры
User
, которая имеет поле с именем email
. Мы хотим установить поле структуры email значением входного параметра email функции build_user
. Так как поле email и входной параметр функции email имеют одинаковое название, можно писать просто email вместо кода email: email
Создание экземпляра структуры из экземпляра другой структуры с
помощью синтаксиса обновления структуры
Часто бывает полезно создать новый экземпляр структуры, который включает большинство значений из другого экземпляра, но некоторые из них изменяет. Это можно сделать с помощью синтаксиса обновления структуры.
Сначала в листинге 5-6 показано, как обычно создаётся новый экземпляр
User в user2
без синтаксиса обновления. Мы задаём новое значение для email
, но в остальном используем те же значения из user1
, которые были заданы в листинге 5-2.
Листинг 5-6: Создание нового экземпляра
User
с использованием некоторых значений из экземпляра
user1
Используя синтаксис обновления структуры, можно получить тот же эффект, используя меньше кода как показано в листинге 5-7. Синтаксис указывает, что оставшиеся поля fn build_user
(email:
String
, username:
String
) -> User {
User { email, username, active: true
, sign_in_count:
1
,
}
} fn main
() {
// --snip-- let user2 = User { active: user1.active, username: user1.username, email:
String
::from(
"another@example.com"
), sign_in_count: user1.sign_in_count,
};
}
устанавливаются неявно и должны иметь значения из указанного экземпляра.
Листинг 5-7: Использование синтаксиса обновления структуры для установки нового значения
email
для
экземпляра
User
, но использование остальных значений из экземпляра
user1
Код в листинге 5-7 также создаёт экземпляр в user2
, который имеет другое значение для email
, но с тем же значением для полей username
, active и sign_in_count из user1
Оператор
..user1
должен стоять последним для указания на получение значений всех оставшихся полей из соответствующих полей в user1
, но можно указать значения для любого количества полей в любом порядке, независимо от порядка полей в определении структуры.
Заметим, что синтаксис обновления структуры использует
=
как присваивание. Это связано с перемещением данных, как мы видели в разделе "Способы взаимодействия переменных и данных: перемещение"
. В этом примере мы больше не можем использовать user1
после создания user2
, потому что
String в поле username из user1
было перемещено в user2
. Если бы мы задали user2
новые значения
String для email и username
, и при этом использовать только значения active и sign_in_count из user1
, то user1
все ещё будет действительным после создания user2
. Типы active и sign_in_count являются типами, реализующими типаж
Copy
, поэтому будет применяться поведение, о котором мы говорили в разделе "Стековые данные:
Копирование"
Кортежные структуры: структуры без именованных полей для
создания разных типов
Rust также поддерживает структуры, похожие на кортежи, которые называются
кортежные структуры. Кортежные структуры обладают дополнительным смыслом,
который даёт имя структуры, но при этом не имеют имён, связанных с их полями. Скорее,
они просто хранят типы полей. Кортежные структуры полезны, когда вы хотите дать имя всему кортежу и сделать кортеж отличным от других кортежей, и когда именование каждого поля, как в обычной структуре, было бы многословным или избыточным.
Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, за которым следуют типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами
Color и
Point
:
fn main
() {
// --snip-- let user2 = User { email:
String
::from(
"another@example.com"
),
..user1
};
}
Обратите внимание, что значения black и origin
— это разные типы, потому что они являются экземплярами разных кортежных структур. Каждая определяемая вами структура имеет собственный тип, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, принимающая параметр типа
Color
, не может принимать
Point в качестве аргумента, даже если оба типа состоят из трёх значений i32
. В остальном экземпляры кортежных структур похожи на кортежи в том смысле, что вы можете деструктурировать их на отдельные части и использовать
, за которой следует индекс для доступа к отдельному значению.
Единично-подобные структуры: структуры без полей
Также можно определять структуры, не имеющие полей! Они называются единично-
подобными структурами, поскольку ведут себя аналогично
()
, единичному типу, о котором мы говорили в разделе "Кортежи"
. Единично-подобные структуры могут быть полезны, когда требуется реализовать типаж для некоторого типа, но у вас нет данных,
которые нужно хранить в самом типе. Мы обсудим типажи в главе 10. Вот пример объявления и создание экземпляра единичной структуры с именем
AlwaysEqual
:
Чтобы определить
AlwaysEqual
, мы используем ключевое слово struct
, желаемое имя,
а затем точку с запятой. Нет необходимости в фигурных или круглых скобках! Затем мы можем получить экземпляр
AlwaysEqual в переменной subject аналогичным образом:
используя имя, которое мы определили, без фигурных и круглых скобок. Представим, что в дальнейшем мы реализуем поведение для этого типа таким образом, что каждый экземпляр
AlwaysEqual всегда будет равен каждому экземпляру любого другого типа,
возможно, с целью получения ожидаемого результата для тестирования. Для реализации такого поведения нам не нужны никакие данные! В главе 10 вы увидите, как определять черты и реализовывать их для любого типа, включая единично-подобные структуры.
Владение данными структуры
struct
Color
(
i32
, i32
, i32
); struct
Point
(
i32
, i32
, i32
); fn main
() { let black = Color(
0
,
0
,
0
); let origin = Point(
0
,
0
,
0
);
} struct
AlwaysEqual
; fn main
() { let subject = AlwaysEqual;
}
В определении структуры
User в листинге 5-1 мы использовали владеющий тип
String вместо типа строковой срез
&str
. Это осознанный выбор, поскольку мы хотим, чтобы каждый экземпляр этой структуры владел всеми своими данными и чтобы эти данные были действительны до тех пор, пока действительна вся структура.
Структуры также могут хранить ссылки на данные, принадлежащие кому-то другому,
но для этого необходимо использовать возможность Rust время жизни, которую мы обсудим в главе 10. Время жизни гарантирует, что данные, на которые ссылается структура, будут действительны до тех пор, пока существует структура. Допустим,
если попытаться сохранить ссылку в структуре без указания времени жизни, как в следующем примере; это не сработает:
Имя файла: src/main.rs
Компилятор будет жаловаться на необходимость определения времени жизни ссылок:
struct
User
{ active: bool
, username: &
str
, email: &
str
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
"someone@example.com"
, username:
"someusername123"
, active: true
, sign_in_count:
1
,
};
}
В главе 10 мы обсудим, как исправить эти ошибки, чтобы иметь возможность хранить ссылки в структурах, а пока мы исправим подобные ошибки, используя владеющие типы вроде
String вместо ссылок
&str
$
cargo run
Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier
-->
src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 username: &'a str,
| error[E0106]: missing lifetime specifier
-->
src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 | username: &str,
4 email: &'a str,
|
For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` due to 2 previous errors
Пример использования структур
Чтобы понять, когда нам может понадобиться использование структур, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.
Давайте создадим новый проект программы при помощи Cargo и назовём его rectangles.
Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Листинг 5-8 показывает один из коротких вариантов кода, который позволит нам сделать именно то, что надо, в файле проекта
src/main.rs.
Файл: src/main.rs
Листинг 5-8: вычисление площади прямоугольника, заданного отдельными переменными ширины и
высоты
Теперь запустим программу, используя cargo run
:
Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area с
каждым измерением, но мы можем улучшить его ясность и читабельность.
Проблема данного метода очевидна из сигнатуры area
:
Функция area должна вычислять площадь одного прямоугольника, но функция, которую мы написали, имеет два параметра, и нигде в нашей программе не ясно, что эти параметры взаимосвязаны. Было бы более читабельным и управляемым сгруппировать fn main
() { let width1 =
30
; let height1 =
50
; println!
(
"The area of the rectangle is {} square pixels."
, area(width1, height1)
);
} fn area
(width: u32
, height: u32
) -> u32
{ width * height
}
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels. fn area
(width: u32
, height: u32
) -> u32
{
ширину и высоту вместе. В разделе
«Кортежи»
главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.
Рефакторинг при помощи кортежей
Листинг 5-9 — это другая версия программы, использующая кортежи.
Файл: src/main.rs
Листинг 5-9: определение ширины и высоты прямоугольника с помощью кортежа
С одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.
Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже с индексом
0
, а высота height
— с индексом
1
. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.
Рефакторинг при помощи структур: добавим больше смысла
Мы используем структуры, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в листинге 5-10.
Файл: src/main.rs fn main
() { let rect1 = (
30
,
50
); println!
(
"The area of the rectangle is {} square pixels."
, area(rect1)
);
} fn area
(dimensions: (
u32
, u32
)) -> u32
{ dimensions.
0
* dimensions.
1
}
Листинг 5-10: определение структуры
Rectangle
Здесь мы определили структуру и дали ей имя
Rectangle
. Внутри фигурных скобок определили поля как width и height
, оба — типа u32
. Затем в main создали конкретный экземпляр
Rectangle с шириной в 30 и высотой в 50 единиц.
Наша функция area теперь определена с одним параметром, названным rectangle
, чей тип является неизменяемым заимствованием структуры
Rectangle
. Как упоминалось в главе 4, необходимо заимствовать структуру, а не передавать её во владение. Таким образом функция main сохраняет rect1
в собственности и может использовать её
дальше. По этой причине мы и используем
&
в сигнатуре и в месте вызова функции.
Функция area получает доступ к полям width и height экземпляра
Rectangle
(обратите внимание, что доступ к полям заимствованного экземпляра структуры не приводит к перемещению значений полей, поэтому вы часто видите заимствования структур). Наша сигнатура функции для area теперь говорит именно то, что мы имеем в виду: вычислить площадь
Rectangle
, используя его поля width и height
. Это означает, что ширина и высота связаны друг с другом, и даёт описательные имена значениям, а не использует значения индекса кортежа
0
и
1
. Это торжество ясности.
Добавление полезной функциональности при помощи выводимых
1 ... 6 7 8 9 10 11 12 13 ... 62
Структуры похожи на кортежи, рассмотренные в разделе "Кортежи"
, так как оба хранят несколько связанных значений. Как и кортежи, части структур могут быть разных типов.
В отличие от кортежей, в структуре необходимо именовать каждую часть данных для понимания смысла значений. Добавление этих имён обеспечивает большую гибкость структур по сравнению с кортежами: не нужно полагаться на порядок данных для указания значений экземпляра или доступа к ним.
Для определения структуры указывается ключевое слово struct и её название.
Название должно описывать значение частей данных, сгруппированных вместе. Далее, в фигурных скобках для каждой новой части данных поочерёдно определяются имя части данных и её тип. Каждая пара имя: тип называется полем. Листинг 5-1 описывает структуру для хранения информации об учётной записи пользователя:
Листинг 5-1: Определение структуры
User
После определения структуры можно создавать её экземпляр, назначая определённое значение каждому полю с соответствующим типом данных. Чтобы создать экземпляр, мы указываем имя структуры, затем добавляем фигурные скобки и включаем в них пары ключ: значение
(key: value), где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании структуры (но всё-таки желательно для удобства чтения). Другими словами, объявление структуры - это как шаблон нашего типа, в то время как экземпляр структуры использует этот шаблон, заполняя его определёнными данными, для создания значений нашего типа. Например, можно объявить пользователя как в листинге 5-2:
Листинг 5-2: Создание экземпляра структуры
User
Чтобы получить конкретное значение из структуры, мы используем запись через точку.
Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы struct
User
{ active: bool
, username:
String
, email:
String
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
};
}
используем user1.email
. Если экземпляр является изменяемым, мы можем поменять значение, используя точечную нотацию и присвоение к конкретному полю. В Листинге
5-3 показано, как изменить значение в поле email изменяемого экземпляра
User
Листинг 5-3: Изменение значения в поле
email
экземпляра
User
Заметим, что весь экземпляр структуры должен быть изменяемым; Rust не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания структуры в качестве последнего выражения тела функции для неявного возврата нового экземпляра.
На листинге 5-4 функция build_user возвращает экземпляр
User с указанным адресом и именем. Поле active получает значение true
, а поле sign_in_count получает значение
1
Листинг 5-4: Функция
build_user
, которая принимает email и имя пользователя и возвращает экземпляр
User
Имеет смысл называть параметры функции теми же именами, что и поля структуры, но необходимость повторять email и username для названий полей и переменных несколько утомительна. Если структура имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!
Использование сокращённой инициализации поля
Так как имена входных параметров функции и полей структуры являются полностью идентичными в листинге 5-4, возможно использовать синтаксис сокращённой
fn main
() { let mut user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
}; user1.email =
String
::from(
"anotheremail@example.com"
);
} fn build_user
(email:
String
, username:
String
) -> User {
User { email: email, username: username, active: true
, sign_in_count:
1
,
}
}
. Если экземпляр является изменяемым, мы можем поменять значение, используя точечную нотацию и присвоение к конкретному полю. В Листинге
5-3 показано, как изменить значение в поле email изменяемого экземпляра
User
Листинг 5-3: Изменение значения в поле
экземпляра
User
Заметим, что весь экземпляр структуры должен быть изменяемым; Rust не позволяет помечать изменяемыми отдельные поля. Как и для любого другого выражения, мы можем использовать выражение создания структуры в качестве последнего выражения тела функции для неявного возврата нового экземпляра.
На листинге 5-4 функция build_user возвращает экземпляр
User с указанным адресом и именем. Поле active получает значение true
, а поле sign_in_count получает значение
1
Листинг 5-4: Функция
build_user
, которая принимает email и имя пользователя и возвращает экземпляр
User
Имеет смысл называть параметры функции теми же именами, что и поля структуры, но необходимость повторять email и username для названий полей и переменных несколько утомительна. Если структура имеет много полей, повторение каждого имени станет ещё более раздражающим. К счастью, есть удобное сокращение!
Использование сокращённой инициализации поля
Так как имена входных параметров функции и полей структуры являются полностью идентичными в листинге 5-4, возможно использовать синтаксис сокращённой
fn main
() { let mut user1 = User { email:
String
::from(
"someone@example.com"
), username:
String
::from(
"someusername123"
), active: true
, sign_in_count:
1
,
}; user1.email =
String
::from(
"anotheremail@example.com"
);
} fn build_user
(email:
String
, username:
String
) -> User {
User { email: email, username: username, active: true
, sign_in_count:
1
,
}
}
инициализации поля, чтобы переписать build_user так, чтобы он работал точно также,
но не содержал повторений для email и username
, как в листинге 5-5.
Листинг 5-5: Функция
build_user
, использующая сокращённую инициализацию поля, когда параметры
и
username
имеют те же имена, что и поля struct
Здесь происходит создание нового экземпляра структуры
User
, которая имеет поле с именем email
. Мы хотим установить поле структуры email значением входного параметра email функции build_user
. Так как поле email и входной параметр функции email имеют одинаковое название, можно писать просто email вместо кода email: email
Создание экземпляра структуры из экземпляра другой структуры с
помощью синтаксиса обновления структуры
Часто бывает полезно создать новый экземпляр структуры, который включает большинство значений из другого экземпляра, но некоторые из них изменяет. Это можно сделать с помощью синтаксиса обновления структуры.
Сначала в листинге 5-6 показано, как обычно создаётся новый экземпляр
User в user2
без синтаксиса обновления. Мы задаём новое значение для email
, но в остальном используем те же значения из user1
, которые были заданы в листинге 5-2.
Листинг 5-6: Создание нового экземпляра
User
с использованием некоторых значений из экземпляра
user1
Используя синтаксис обновления структуры, можно получить тот же эффект, используя меньше кода как показано в листинге 5-7. Синтаксис указывает, что оставшиеся поля fn build_user
(email:
String
, username:
String
) -> User {
User { email, username, active: true
, sign_in_count:
1
,
}
} fn main
() {
// --snip-- let user2 = User { active: user1.active, username: user1.username, email:
String
::from(
"another@example.com"
), sign_in_count: user1.sign_in_count,
};
}
устанавливаются неявно и должны иметь значения из указанного экземпляра.
Листинг 5-7: Использование синтаксиса обновления структуры для установки нового значения
email
для
экземпляра
User
, но использование остальных значений из экземпляра
user1
Код в листинге 5-7 также создаёт экземпляр в user2
, который имеет другое значение для email
, но с тем же значением для полей username
, active и sign_in_count из user1
Оператор
..user1
должен стоять последним для указания на получение значений всех оставшихся полей из соответствующих полей в user1
, но можно указать значения для любого количества полей в любом порядке, независимо от порядка полей в определении структуры.
Заметим, что синтаксис обновления структуры использует
=
как присваивание. Это связано с перемещением данных, как мы видели в разделе "Способы взаимодействия переменных и данных: перемещение"
. В этом примере мы больше не можем использовать user1
после создания user2
, потому что
String в поле username из user1
было перемещено в user2
. Если бы мы задали user2
новые значения
String для email и username
, и при этом использовать только значения active и sign_in_count из user1
, то user1
все ещё будет действительным после создания user2
. Типы active и sign_in_count являются типами, реализующими типаж
Copy
, поэтому будет применяться поведение, о котором мы говорили в разделе "Стековые данные:
Копирование"
Кортежные структуры: структуры без именованных полей для
создания разных типов
Rust также поддерживает структуры, похожие на кортежи, которые называются
кортежные структуры. Кортежные структуры обладают дополнительным смыслом,
который даёт имя структуры, но при этом не имеют имён, связанных с их полями. Скорее,
они просто хранят типы полей. Кортежные структуры полезны, когда вы хотите дать имя всему кортежу и сделать кортеж отличным от других кортежей, и когда именование каждого поля, как в обычной структуре, было бы многословным или избыточным.
Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, за которым следуют типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами
Color и
Point
:
fn main
() {
// --snip-- let user2 = User { email:
String
::from(
"another@example.com"
),
..user1
};
}
Листинг 5-7: Использование синтаксиса обновления структуры для установки нового значения
для
экземпляра
User
, но использование остальных значений из экземпляра
user1
Код в листинге 5-7 также создаёт экземпляр в user2
, который имеет другое значение для email
, но с тем же значением для полей username
, active и sign_in_count из user1
Оператор
..user1
должен стоять последним для указания на получение значений всех оставшихся полей из соответствующих полей в user1
, но можно указать значения для любого количества полей в любом порядке, независимо от порядка полей в определении структуры.
Заметим, что синтаксис обновления структуры использует
=
как присваивание. Это связано с перемещением данных, как мы видели в разделе "Способы взаимодействия переменных и данных: перемещение"
. В этом примере мы больше не можем использовать user1
после создания user2
, потому что
String в поле username из user1
было перемещено в user2
. Если бы мы задали user2
новые значения
String для email и username
, и при этом использовать только значения active и sign_in_count из user1
, то user1
все ещё будет действительным после создания user2
. Типы active и sign_in_count являются типами, реализующими типаж
Copy
, поэтому будет применяться поведение, о котором мы говорили в разделе "Стековые данные:
Копирование"
Кортежные структуры: структуры без именованных полей для
создания разных типов
Rust также поддерживает структуры, похожие на кортежи, которые называются
кортежные структуры. Кортежные структуры обладают дополнительным смыслом,
который даёт имя структуры, но при этом не имеют имён, связанных с их полями. Скорее,
они просто хранят типы полей. Кортежные структуры полезны, когда вы хотите дать имя всему кортежу и сделать кортеж отличным от других кортежей, и когда именование каждого поля, как в обычной структуре, было бы многословным или избыточным.
Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, за которым следуют типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами
Color и
Point
:
fn main
() {
// --snip-- let user2 = User { email:
String
::from(
"another@example.com"
),
..user1
};
}
Обратите внимание, что значения black и origin
— это разные типы, потому что они являются экземплярами разных кортежных структур. Каждая определяемая вами структура имеет собственный тип, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, принимающая параметр типа
Color
, не может принимать
Point в качестве аргумента, даже если оба типа состоят из трёх значений i32
. В остальном экземпляры кортежных структур похожи на кортежи в том смысле, что вы можете деструктурировать их на отдельные части и использовать
, за которой следует индекс для доступа к отдельному значению.
Единично-подобные структуры: структуры без полей
Также можно определять структуры, не имеющие полей! Они называются единично-
подобными структурами, поскольку ведут себя аналогично
()
, единичному типу, о котором мы говорили в разделе "Кортежи"
. Единично-подобные структуры могут быть полезны, когда требуется реализовать типаж для некоторого типа, но у вас нет данных,
которые нужно хранить в самом типе. Мы обсудим типажи в главе 10. Вот пример объявления и создание экземпляра единичной структуры с именем
AlwaysEqual
:
Чтобы определить
AlwaysEqual
, мы используем ключевое слово struct
, желаемое имя,
а затем точку с запятой. Нет необходимости в фигурных или круглых скобках! Затем мы можем получить экземпляр
AlwaysEqual в переменной subject аналогичным образом:
используя имя, которое мы определили, без фигурных и круглых скобок. Представим, что в дальнейшем мы реализуем поведение для этого типа таким образом, что каждый экземпляр
AlwaysEqual всегда будет равен каждому экземпляру любого другого типа,
возможно, с целью получения ожидаемого результата для тестирования. Для реализации такого поведения нам не нужны никакие данные! В главе 10 вы увидите, как определять черты и реализовывать их для любого типа, включая единично-подобные структуры.
Владение данными структуры
struct
Color
(
i32
, i32
, i32
); struct
Point
(
i32
, i32
, i32
); fn main
() { let black = Color(
0
,
0
,
0
); let origin = Point(
0
,
0
,
0
);
} struct
AlwaysEqual
; fn main
() { let subject = AlwaysEqual;
}
В определении структуры
User в листинге 5-1 мы использовали владеющий тип
String вместо типа строковой срез
&str
. Это осознанный выбор, поскольку мы хотим, чтобы каждый экземпляр этой структуры владел всеми своими данными и чтобы эти данные были действительны до тех пор, пока действительна вся структура.
Структуры также могут хранить ссылки на данные, принадлежащие кому-то другому,
но для этого необходимо использовать возможность Rust время жизни, которую мы обсудим в главе 10. Время жизни гарантирует, что данные, на которые ссылается структура, будут действительны до тех пор, пока существует структура. Допустим,
если попытаться сохранить ссылку в структуре без указания времени жизни, как в следующем примере; это не сработает:
Имя файла: src/main.rs
Компилятор будет жаловаться на необходимость определения времени жизни ссылок:
struct
User
{ active: bool
, username: &
str
, email: &
str
, sign_in_count: u64
,
} fn main
() { let user1 = User { email:
"someone@example.com"
, username:
"someusername123"
, active: true
, sign_in_count:
1
,
};
}
В главе 10 мы обсудим, как исправить эти ошибки, чтобы иметь возможность хранить ссылки в структурах, а пока мы исправим подобные ошибки, используя владеющие типы вроде
String вместо ссылок
&str
$
cargo run
Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier
-->
src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 username: &'a str,
| error[E0106]: missing lifetime specifier
-->
src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
| help: consider introducing a named lifetime parameter
|
1 struct User<'a> {
2 | active: bool,
3 | username: &str,
4 email: &'a str,
|
For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` due to 2 previous errors
Пример использования структур
Чтобы понять, когда нам может понадобиться использование структур, давайте напишем программу, которая вычисляет площадь прямоугольника. Мы начнём с использования одиночных переменных, а затем будем улучшать программу до использования структур.
Давайте создадим новый проект программы при помощи Cargo и назовём его rectangles.
Наша программа будет получать на вход длину и ширину прямоугольника в пикселях и затем рассчитывать площадь прямоугольника. Листинг 5-8 показывает один из коротких вариантов кода, который позволит нам сделать именно то, что надо, в файле проекта
src/main.rs.
Файл: src/main.rs
Листинг 5-8: вычисление площади прямоугольника, заданного отдельными переменными ширины и
высоты
Теперь запустим программу, используя cargo run
:
Этот код успешно вычисляет площадь прямоугольника, вызывая функцию area с
каждым измерением, но мы можем улучшить его ясность и читабельность.
Проблема данного метода очевидна из сигнатуры area
:
Функция area должна вычислять площадь одного прямоугольника, но функция, которую мы написали, имеет два параметра, и нигде в нашей программе не ясно, что эти параметры взаимосвязаны. Было бы более читабельным и управляемым сгруппировать fn main
() { let width1 =
30
; let height1 =
50
; println!
(
"The area of the rectangle is {} square pixels."
, area(width1, height1)
);
} fn area
(width: u32
, height: u32
) -> u32
{ width * height
}
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels. fn area
(width: u32
, height: u32
) -> u32
{
ширину и высоту вместе. В разделе
«Кортежи»
главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.
Рефакторинг при помощи кортежей
Листинг 5-9 — это другая версия программы, использующая кортежи.
Файл: src/main.rs
Листинг 5-9: определение ширины и высоты прямоугольника с помощью кортежа
С одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.
Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже с индексом
0
, а высота height
— с индексом
1
. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.
Рефакторинг при помощи структур: добавим больше смысла
Мы используем структуры, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в листинге 5-10.
Файл: src/main.rs fn main
() { let rect1 = (
30
,
50
); println!
(
"The area of the rectangle is {} square pixels."
, area(rect1)
);
} fn area
(dimensions: (
u32
, u32
)) -> u32
{ dimensions.
0
* dimensions.
1
}
«Кортежи»
главы 3 мы уже обсуждали один из способов сделать это — использовать кортежи.
Рефакторинг при помощи кортежей
Листинг 5-9 — это другая версия программы, использующая кортежи.
Файл: src/main.rs
Листинг 5-9: определение ширины и высоты прямоугольника с помощью кортежа
С одной стороны, эта программа лучше. Кортежи позволяют добавить немного структуры, и теперь мы передаём только один аргумент. Но с другой стороны, эта версия менее понятна: кортежи не называют свои элементы, поэтому нам приходится индексировать части кортежа, что делает наше вычисление менее очевидным.
Если мы перепутаем местами ширину с высотой при расчёте площади, то это не имеет значения. Но если мы хотим нарисовать прямоугольник на экране, то это уже будет важно! Мы должны помнить, что ширина width находится в кортеже с индексом
0
, а высота height
— с индексом
1
. Если кто-то другой поработал бы с кодом, ему бы пришлось разобраться в этом и также помнить про порядок. Легко забыть и перепутать эти значения — и это вызовет ошибки, потому что данный код не передаёт наши намерения.
Рефакторинг при помощи структур: добавим больше смысла
Мы используем структуры, чтобы добавить смысл данным при помощи назначения им осмысленных имён . Мы можем переделать используемый кортеж в структуру с единым именем для сущности и частными названиями её частей, как показано в листинге 5-10.
Файл: src/main.rs fn main
() { let rect1 = (
30
,
50
); println!
(
"The area of the rectangle is {} square pixels."
, area(rect1)
);
} fn area
(dimensions: (
u32
, u32
)) -> u32
{ dimensions.
0
* dimensions.
1
}
Листинг 5-10: определение структуры
Rectangle
Здесь мы определили структуру и дали ей имя
Rectangle
. Внутри фигурных скобок определили поля как width и height
, оба — типа u32
. Затем в main создали конкретный экземпляр
Rectangle с шириной в 30 и высотой в 50 единиц.
Наша функция area теперь определена с одним параметром, названным rectangle
, чей тип является неизменяемым заимствованием структуры
Rectangle
. Как упоминалось в главе 4, необходимо заимствовать структуру, а не передавать её во владение. Таким образом функция main сохраняет rect1
в собственности и может использовать её
дальше. По этой причине мы и используем
&
в сигнатуре и в месте вызова функции.
Функция area получает доступ к полям width и height экземпляра
Rectangle
(обратите внимание, что доступ к полям заимствованного экземпляра структуры не приводит к перемещению значений полей, поэтому вы часто видите заимствования структур). Наша сигнатура функции для area теперь говорит именно то, что мы имеем в виду: вычислить площадь
Rectangle
, используя его поля width и height
. Это означает, что ширина и высота связаны друг с другом, и даёт описательные имена значениям, а не использует значения индекса кортежа
0
и
1
. Это торжество ясности.
Добавление полезной функциональности при помощи выводимых
1 ... 6 7 8 9 10 11 12 13 ... 62
типажей
Было бы полезно иметь возможность печатать экземпляр
Rectangle во время отладки программы и видеть значения всех полей. Листинг 5-11 использует макрос println!
,
который мы уже использовали в предыдущих главах. Тем не менее, это не работает.
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"The area of the rectangle is {} square pixels."
, area(&rect1)
);
} fn area
(rectangle: &Rectangle) -> u32
{ rectangle.width * rectangle.height
}
Файл: src/main.rs
Листинг 5-11: Попытка вывести значения экземпляра
Rectangle
При компиляции этого кода мы получаем ошибку с сообщением:
Макрос println!
умеет выполнять множество видов форматирования, и по умолчанию фигурные скобки в println!
означают использование форматирование, известное как типаж
Display
. Его вывод предназначен для непосредственного использования конечным пользователем. Примитивные типы, изученные ранее, по умолчанию реализуют типаж
Display
, потому что есть только один способ отобразить число
1
или любой другой примитивный тип. Но для структур форматирование println!
менее очевидно, потому что есть гораздо больше способов отображения: Вы хотите запятые или нет? Вы хотите печатать фигурные скобки? Должны ли отображаться все поля? Из-за этой неоднозначности Rust не пытается угадать, что нам нужно, а структуры не имеют встроенной реализации
Display для использования в println!
с заполнителем
{}
Продолжив чтение текста ошибки, мы найдём полезное замечание:
Давайте попробуем! Вызов макроса println!
теперь будет выглядеть так println!
("rect1 is {:?}", rect1);
. Ввод спецификатора
:?
внутри фигурных скобок говорит макросу println!
, что мы хотим использовать другой формат вывода, известный как
Debug
. Типаж
Debug позволяет печатать структуру способом, удобным для разработчиков, чтобы видеть значение во время отладки кода.
Скомпилируем код с этими изменениями. Упс! Мы всё ещё получаем ошибку:
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"rect1 is {}"
, rect1);
} error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty- print) instead error[E0277]: `Rectangle` doesn't implement `Debug`
Снова компилятор даёт нам полезное замечание:
Rust реализует функциональность для печати отладочной информации, но не включает
(не выводит) её по умолчанию. Мы должны явно включить эту функциональность для нашей структуры. Чтобы это сделать, добавляем внешний атрибут
#[derive(Debug)]
сразу перед определением структуры, как показано в листинге 5-12.
Файл: src/main.rs
Листинг 5-12: добавление атрибута для вывода типажа
Debug
и печати экземпляра
Rectangle
с
отладочным форматированием
Теперь при запуске программы мы не получим ошибок и увидим следующий вывод:
Отлично! Это не самый красивый вывод, но он показывает значения всех полей экземпляра, которые определённо помогут при отладке. Когда у нас более крупные структуры, то полезно иметь более простой для чтения вывод; в таких случаях можно использовать код
{:#?}
вместо
{:?}
в строке макроса println!
. В этом примере использование стиля
{:#?}
приведёт к такому выводу:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for
Rectangle`
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"rect1 is {:?}"
, rect1);
}
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50 }
Другой способ распечатать значение в формате
Debug
— использовать макрос dbg!
,
который становится владельцем выражения (в отличие от println!
, принимающего ссылку), печатает номер файла и строки, где происходит вызов макроса dbg!
, вместе с результирующим значением этого выражения и возвращает владение на значение.
Примечание: при вызове макроса dbg!
выполняется печать в стандартный поток ошибок (
stderr
), в отличие от println!
, который использует стандартный поток вывода в консоль (
stdout
). Подробнее о stderr и stdout мы поговорим в разделе
«Запись сообщений об ошибках в стандартный вывод ошибок вместо стандартного вывода» главы 12
Вот пример, когда нас интересует значение, которое присваивается полю width
, а также значение всей структуры в rect1
:
Можем написать макрос dbg!
вокруг выражения
30 * scale
, потому что dbg!
возвращает владение значения выражения. Поле width получит то же значение, как если бы у нас не было вызова dbg!
. Мы не хотим, чтобы макрос dbg!
становился владельцем rect1
, поэтому используем ссылку на rect1
в следующем вызове. Вот как выглядит вывод этого примера:
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles` rect1 is Rectangle { width: 30, height: 50,
}
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} fn main
() { let scale =
2
; let rect1 = Rectangle { width: dbg!(
30
* scale), height:
50
,
}; dbg!(&rect1);
}
Мы можем увидеть, что первый отладочный вывод поступил из строки 10 src/main.rs, там,
где мы отлаживаем выражение
30 * scale
, и его результирующее значение равно 60
(
Debug форматирование, реализованное для целых чисел, заключается в печати только их значения). Вызов dbg!
в строке 14 src/main.rs выводит значение
&rect1
, которое является структурой
Rectangle
. В этом выводе используется красивое форматирование
Debug типа
Rectangle
. Макрос dbg!
может быть очень полезен, когда вы пытаетесь понять, что делает ваш код!
В дополнение к
Debug
, Rust предоставил нам ряд типажей, которые мы можем использовать с атрибутом derive для добавления полезного поведения к нашим пользовательским типам. Эти типажи и их поведение перечислены в приложении C
. Мы расскажем, как реализовать эти трейты с пользовательским поведением, а также как создать свои собственные трейты в главе 10. Кроме того, есть много других атрибутов помимо derive
; для получения дополнительной информации смотрите раздел
“Атрибуты” справочника Rust
Функция area является довольно специфичной: она считает только площадь прямоугольников. Было бы полезно привязать данное поведение как можно ближе к структуре
Rectangle
, чтобы он не мог работать с любым другим типом. Давайте рассмотрим, как можно улучшить наш код, превращая функцию area в метод area
,
определённый для типа
Rectangle
$
cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50,
}
Синтаксис метода
Методы похожи на функции: мы объявляем их с помощью ключевого слова fn и имени,
они могут иметь параметры и возвращаемое значение, и они содержат код,
запускающийся в случае вызова метода. В отличие от функций, методы определяются в контексте структуры (или перечисления, или типаж-объекта, которые мы рассмотрим в
Главах 6 и 17 соответственно), и их первым параметром всегда является self
,
представляющий собой экземпляр структуры, на которой вызывается этот метод.
Определение методов
Давайте изменим функцию area так, чтобы она имела экземпляр
Rectangle в качестве входного параметра и сделаем её методом area
, определённым для структуры
Rectangle
, как показано в листинге 5-13:
Файл: src/main.rs
Листинг 5-13: Определение метода
area
для структуры
Rectangle
Чтобы определить функцию в контексте
Rectangle
, мы создаём блок impl
(implementation - реализация) для
Rectangle
. Всё в impl будет связано с типом
Rectangle
. Затем мы перемещаем функцию area внутрь фигурных скобок impl и
меняем первый (и в данном случае единственный) параметр на self в сигнатуре и в теле. В main
, где мы вызвали функцию area и передали rect1
в качестве аргумента,
теперь мы можем использовать синтаксис метода для вызова метода area нашего
#[derive(Debug)]
struct
Rectangle
{ width: u32
, height: u32
,
} impl
Rectangle { fn area
(&
self
) -> u32
{ self
.width * self
.height
}
} fn main
() { let rect1 = Rectangle { width:
30
, height:
50
,
}; println!
(
"The area of the rectangle is {} square pixels."
, rect1.area()
);
}
экземпляра
Rectangle
. Синтаксис метода идёт после экземпляра: мы добавляем точку, за которой следует имя метода, круглые скобки и любые аргументы.
В сигнатуре area мы используем
&self вместо rectangle: &Rectangle
&self на самом деле является сокращением от self: &Self
. Внутри блока impl тип
Self является псевдонимом типа, для которого реализован блок impl
. Методы обязаны иметь параметр с именем self типа
Self
, поэтому Rust позволяет вам сокращать его,
используя только имя self на месте первого параметра. Обратите внимание, что нам по-прежнему нужно использовать
&
перед сокращением self
, чтобы указать на то, что этот метод заимствует экземпляр
Self
, точно так же, как мы делали это в rectangle:
&Rectangle
. Как и любой другой параметр, методы могут брать во владение self
,
заимствовать неизменяемый self
, как мы поступили в данном случае, или заимствовать изменяемый self
Мы выбрали
&self здесь по той же причине, по которой использовали
&Rectangle в
версии кода с функцией: мы не хотим брать структуру во владение, мы просто хотим прочитать данные в структуре, а не писать в неё. Если бы мы хотели изменить экземпляр,
на котором мы вызывали метод силами самого метода, то мы бы использовали
&mut self в качестве первого параметра. Наличие метода, который берёт экземпляр во владение, используя только self в качестве первого параметра, является редким; эта техника обычно используется, когда метод превращает self во что-то ещё, и вы хотите запретить вызывающей стороне использовать исходный экземпляр после превращения.
Основная причина использования методов вместо функций, помимо синтаксиса метода,
где нет необходимости повторять тип self в сигнатуре каждого метода, заключается в организации кода. Мы поместили все, что мы можем сделать с экземпляром типа, в один impl вместо того, чтобы заставлять будущих пользователей нашего кода искать доступный функционал
Rectangle в разных местах предоставляемой нами библиотеки.
Обратите внимание, что мы можем дать методу то же имя, что и одному из полей структуры. Например, для
Rectangle мы можем определить метод, также названный width
:
Файл: src/main.rs
Rectangle
. Синтаксис метода идёт после экземпляра: мы добавляем точку, за которой следует имя метода, круглые скобки и любые аргументы.
В сигнатуре area мы используем
&self вместо rectangle: &Rectangle
&self на самом деле является сокращением от self: &Self
. Внутри блока impl тип
Self является псевдонимом типа, для которого реализован блок impl
. Методы обязаны иметь параметр с именем self типа
Self
, поэтому Rust позволяет вам сокращать его,
используя только имя self на месте первого параметра. Обратите внимание, что нам по-прежнему нужно использовать
&
перед сокращением self
, чтобы указать на то, что этот метод заимствует экземпляр
Self
, точно так же, как мы делали это в rectangle:
&Rectangle
. Как и любой другой параметр, методы могут брать во владение self
,
заимствовать неизменяемый self
, как мы поступили в данном случае, или заимствовать изменяемый self
Мы выбрали
&self здесь по той же причине, по которой использовали
&Rectangle в
версии кода с функцией: мы не хотим брать структуру во владение, мы просто хотим прочитать данные в структуре, а не писать в неё. Если бы мы хотели изменить экземпляр,
на котором мы вызывали метод силами самого метода, то мы бы использовали
&mut self в качестве первого параметра. Наличие метода, который берёт экземпляр во владение, используя только self в качестве первого параметра, является редким; эта техника обычно используется, когда метод превращает self во что-то ещё, и вы хотите запретить вызывающей стороне использовать исходный экземпляр после превращения.
Основная причина использования методов вместо функций, помимо синтаксиса метода,
где нет необходимости повторять тип self в сигнатуре каждого метода, заключается в организации кода. Мы поместили все, что мы можем сделать с экземпляром типа, в один impl вместо того, чтобы заставлять будущих пользователей нашего кода искать доступный функционал
Rectangle в разных местах предоставляемой нами библиотеки.
Обратите внимание, что мы можем дать методу то же имя, что и одному из полей структуры. Например, для
Rectangle мы можем определить метод, также названный width
:
Файл: src/main.rs