Определение и создание структур
Структуры похожи на кортежи, о которых шла речь в разделе «Кортежи»: и те и другие хранят несколько связанных значений. Как и в кортеже, части структуры могут быть разных типов. Но, в отличие от кортежа, в структуре у каждой части данных есть имя, поэтому сразу понятно, что означает каждое значение. Благодаря именам структуры гибче кортежей: чтобы задать или прочитать значение экземпляра, не нужно помнить порядок данных.
Чтобы определить структуру, пишем ключевое слово struct и даём имя всей
структуре. Имя должно отражать смысл данных, которые в ней сгруппированы. Затем
в фигурных скобках перечисляем имена и типы частей данных — они называются
полями (fields). Например, в листинге 5-1 показана структура, которая
хранит сведения об учётной записи пользователя.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
UserЧтобы пользоваться определённой структурой, нужно создать её экземпляр
(instance), указав конкретные значения всех полей. Для этого пишем имя
структуры, а за ним — фигурные скобки с парами ключ: значение, где ключ —
имя поля, а значение — данные, которые мы хотим в нём хранить. Порядок полей
не обязан совпадать с порядком в объявлении структуры. Иначе говоря,
определение структуры — это общий шаблон типа, а экземпляры заполняют шаблон
конкретными данными и так создают значения этого типа. Например, конкретного
пользователя можно объявить так, как в листинге 5-2.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
UserЧтобы получить конкретное значение из структуры, используется точечная
нотация. Например, адрес электронной почты этого пользователя — это
user1.email. Если экземпляр изменяемый, значение можно поменять, обратившись
к полю через точку и присвоив ему новое значение. В листинге 5-3 показано, как
изменить поле email у изменяемого экземпляра User.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
email у экземпляра UserОбратите внимание: изменяемым должен быть весь экземпляр целиком — пометить изменяемыми только отдельные поля Rust не позволяет. Как и любое другое выражение, новый экземпляр структуры можно создать последним выражением в теле функции — тогда функция неявно вернёт его.
В листинге 5-4 показана функция build_user, которая возвращает экземпляр
User с переданными адресом почты и именем пользователя. Поле active
получает значение true, а sign_in_count — 1.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
build_user принимает адрес почты и имя пользователя и возвращает экземпляр UserНазывать параметры функции так же, как поля структуры, вполне логично, но
повторять email и username дважды — и как имена полей, и как имена
переменных — немного утомительно. А будь у структуры больше полей, это
раздражало бы ещё сильнее. К счастью, есть удобное сокращение!
Сокращённая инициализация полей
В листинге 5-4 имена параметров и имена полей структуры полностью совпадают.
Поэтому build_user можно переписать с помощью сокращённой инициализации
полей (field init shorthand): функция будет вести себя точно так же, но без
повторения username и email, как в листинге 5-5.
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
build_user использует сокращённую инициализацию, так как параметры username и email называются так же, как поля структурыЗдесь мы создаём новый экземпляр структуры User, у которой есть поле
email. В это поле мы хотим записать значение параметра email функции
build_user. Раз поле и параметр называются одинаково, достаточно написать
email вместо email: email.
Создание экземпляров с синтаксисом обновления
Часто бывает нужно создать новый экземпляр структуры, взяв большую часть значений из другого экземпляра того же типа и поменяв лишь некоторые. Для этого есть синтаксис обновления структуры (struct update syntax).
Сначала в листинге 5-6 покажем, как создать новый экземпляр User в user2
обычным способом, без синтаксиса обновления. Мы задаём новое значение email,
а остальные значения берём из user1, созданного в листинге 5-2.
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,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
User, взяв из user1 все значения, кроме одногоС синтаксисом обновления того же можно добиться меньшим количеством кода, как
в листинге 5-7. Запись .. означает, что все остальные поля, которые не заданы
явно, получат те же значения, что и в указанном экземпляре.
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,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
User новый email, а остальные значения берём из user1Код в листинге 5-7 тоже создаёт экземпляр user2 с другим значением email,
но с теми же значениями полей username, active и sign_in_count, что и у
user1. ..user1 должно стоять последним: оно говорит, что все оставшиеся
поля берут значения из соответствующих полей user1. При этом явно задать
можно сколько угодно полей и в любом порядке, независимо от порядка полей в
определении структуры.
Обратите внимание: синтаксис обновления работает как присваивание через =,
то есть перемещает данные (см. раздел «Переменные и данные:
перемещение»). Что из этого следует в нашем примере:
- Поле
username— этоString, и её значение переместилось изuser1вuser2. Поэтомуuser1целиком использовать больше нельзя. - Поле
emailмы задали дляuser2заново, так чтоuser1.emailникуда не перемещалось — к нему по-прежнему можно обращаться. - Поля
activeиsign_in_countимеют типы, реализующие трейтCopy, поэтому они не перемещаются, а копируются (см. раздел «Данные только на стеке: копирование»).
Если бы мы задали для user2 новые String и в email, и в username, а из
user1 взяли бы только active и sign_in_count, то user1 осталась бы
полностью действительной и после создания user2.
Кортежные структуры: разные типы из похожих кортежей
Rust поддерживает и структуры, похожие на кортежи, — кортежные структуры (tuple structs). У кортежной структуры есть имя, которое придаёт ей смысл, но у её полей имён нет — только типы. Кортежные структуры удобны, когда нужно дать имя всему кортежу и сделать его отдельным типом, отличным от других кортежей, а давать имя каждому полю, как в обычной структуре, было бы многословно или излишне.
Чтобы определить кортежную структуру, напишите ключевое слово struct, имя
структуры, а за ним — типы в кортеже. Например, вот определение и
использование двух кортежных структур — Color и Point:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
Обратите внимание: значения black и origin — разных типов, потому что это
экземпляры разных кортежных структур. Каждая определённая вами структура — это
отдельный тип, даже если типы полей у структур совпадают. Например, функция,
принимающая параметр типа Color, не примет аргумент типа Point, хотя оба
типа состоят из трёх значений i32. В остальном экземпляры кортежных структур
ведут себя как кортежи: их можно разобрать на отдельные части и обращаться к
отдельному значению через . и индекс. Но, в отличие от кортежей, при
деструктуризации кортежной структуры нужно указывать её тип. Например, чтобы
разложить значения точки origin по переменным x, y и z, мы бы
написали let Point(x, y, z) = origin;.
Единичные структуры без полей
Можно определить и структуру вообще без полей! Такие структуры называют
единичными (unit-like structs), потому что они ведут себя похоже на () —
единичный тип, который мы упоминали в разделе «Кортежи». Единичные структуры пригодятся, когда нужно реализовать для какого-то
типа трейт, но хранить в самом типе никаких данных не требуется. Трейты мы
обсудим в главе 10. Вот пример объявления и создания экземпляра единичной
структуры AlwaysEqual:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
Чтобы определить AlwaysEqual, пишем ключевое слово struct, нужное имя и
точку с запятой. Ни фигурных, ни круглых скобок не нужно! Экземпляр
AlwaysEqual в переменной subject получаем так же просто — по имени, без
всяких скобок. Представьте, что позже мы реализуем для этого типа поведение, при
котором любой экземпляр AlwaysEqual всегда равен любому экземпляру любого
другого типа, — например, чтобы получать заранее известный результат в
тестах. Для такого поведения никакие данные не нужны! В главе 10 вы увидите,
как определять трейты и реализовывать их для любых типов, в том числе для
единичных структур.
Владение данными структуры
В определении структуры User из листинга 5-1 мы использовали тип String,
который владеет своими данными, а не строковый срез &str. Это сознательный
выбор: мы хотим, чтобы каждый экземпляр структуры владел всеми своими
данными и чтобы эти данные оставались действительными, пока действительна
вся структура.
Структуры могут хранить и ссылки на данные, которыми владеет кто-то другой, но для этого нужны времена жизни (lifetimes) — о них мы поговорим в главе 10. Времена жизни гарантируют, что данные, на которые ссылается структура, действительны, пока действительна сама структура. Допустим, вы пытаетесь сохранить в структуре ссылку, не указав времена жизни, — как в следующем коде в src/main.rs. Так не выйдет:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "someone@example.com",
sign_in_count: 1,
};
}
Компилятор пожалуется, что ему нужны спецификаторы времени жизни:
$ 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` (bin "structs") due to 2 previous errors
В главе 10 мы разберёмся, как исправить эти ошибки и хранить в структурах
ссылки, а пока будем избегать таких ошибок, используя типы-владельцы вроде
String вместо ссылок вроде &str.