Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Определение и создание структур

Структуры похожи на кортежи, о которых шла речь в разделе «Кортежи»: и те и другие хранят несколько связанных значений. Как и в кортеже, части структуры могут быть разных типов. Но, в отличие от кортежа, в структуре у каждой части данных есть имя, поэтому сразу понятно, что означает каждое значение. Благодаря именам структуры гибче кортежей: чтобы задать или прочитать значение экземпляра, не нужно помнить порядок данных.

Чтобы определить структуру, пишем ключевое слово struct и даём имя всей структуре. Имя должно отражать смысл данных, которые в ней сгруппированы. Затем в фигурных скобках перечисляем имена и типы частей данных — они называются полями (fields). Например, в листинге 5-1 показана структура, которая хранит сведения об учётной записи пользователя.

Файл: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
Листинг 5-1: Определение структуры User

Чтобы пользоваться определённой структурой, нужно создать её экземпляр (instance), указав конкретные значения всех полей. Для этого пишем имя структуры, а за ним — фигурные скобки с парами ключ: значение, где ключ — имя поля, а значение — данные, которые мы хотим в нём хранить. Порядок полей не обязан совпадать с порядком в объявлении структуры. Иначе говоря, определение структуры — это общий шаблон типа, а экземпляры заполняют шаблон конкретными данными и так создают значения этого типа. Например, конкретного пользователя можно объявить так, как в листинге 5-2.

Файл: src/main.rs
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,
    };
}
Листинг 5-2: Создаём экземпляр структуры User

Чтобы получить конкретное значение из структуры, используется точечная нотация. Например, адрес электронной почты этого пользователя — это user1.email. Если экземпляр изменяемый, значение можно поменять, обратившись к полю через точку и присвоив ему новое значение. В листинге 5-3 показано, как изменить поле email у изменяемого экземпляра User.

Файл: src/main.rs
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");
}
Листинг 5-3: Меняем значение поля email у экземпляра User

Обратите внимание: изменяемым должен быть весь экземпляр целиком — пометить изменяемыми только отдельные поля Rust не позволяет. Как и любое другое выражение, новый экземпляр структуры можно создать последним выражением в теле функции — тогда функция неявно вернёт его.

В листинге 5-4 показана функция build_user, которая возвращает экземпляр User с переданными адресом почты и именем пользователя. Поле active получает значение true, а sign_in_count — 1.

Файл: src/main.rs
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"),
    );
}
Листинг 5-4: Функция build_user принимает адрес почты и имя пользователя и возвращает экземпляр User

Называть параметры функции так же, как поля структуры, вполне логично, но повторять email и username дважды — и как имена полей, и как имена переменных — немного утомительно. А будь у структуры больше полей, это раздражало бы ещё сильнее. К счастью, есть удобное сокращение!

Сокращённая инициализация полей

В листинге 5-4 имена параметров и имена полей структуры полностью совпадают. Поэтому build_user можно переписать с помощью сокращённой инициализации полей (field init shorthand): функция будет вести себя точно так же, но без повторения username и email, как в листинге 5-5.

Файл: src/main.rs
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"),
    );
}
Листинг 5-5: Функция build_user использует сокращённую инициализацию, так как параметры username и email называются так же, как поля структуры

Здесь мы создаём новый экземпляр структуры User, у которой есть поле email. В это поле мы хотим записать значение параметра email функции build_user. Раз поле и параметр называются одинаково, достаточно написать email вместо email: email.

Создание экземпляров с синтаксисом обновления

Часто бывает нужно создать новый экземпляр структуры, взяв большую часть значений из другого экземпляра того же типа и поменяв лишь некоторые. Для этого есть синтаксис обновления структуры (struct update syntax).

Сначала в листинге 5-6 покажем, как создать новый экземпляр User в user2 обычным способом, без синтаксиса обновления. Мы задаём новое значение email, а остальные значения берём из user1, созданного в листинге 5-2.

Файл: src/main.rs
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,
    };
}
Листинг 5-6: Создаём новый экземпляр User, взяв из user1 все значения, кроме одного

С синтаксисом обновления того же можно добиться меньшим количеством кода, как в листинге 5-7. Запись .. означает, что все остальные поля, которые не заданы явно, получат те же значения, что и в указанном экземпляре.

Файл: src/main.rs
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
    };
}
Листинг 5-7: С помощью синтаксиса обновления задаём экземпляру 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:

Файл: src/main.rs
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:

Файл: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Чтобы определить AlwaysEqual, пишем ключевое слово struct, нужное имя и точку с запятой. Ни фигурных, ни круглых скобок не нужно! Экземпляр AlwaysEqual в переменной subject получаем так же просто — по имени, без всяких скобок. Представьте, что позже мы реализуем для этого типа поведение, при котором любой экземпляр AlwaysEqual всегда равен любому экземпляру любого другого типа, — например, чтобы получать заранее известный результат в тестах. Для такого поведения никакие данные не нужны! В главе 10 вы увидите, как определять трейты и реализовывать их для любых типов, в том числе для единичных структур.

Владение данными структуры

В определении структуры User из листинга 5-1 мы использовали тип String, который владеет своими данными, а не строковый срез &str. Это сознательный выбор: мы хотим, чтобы каждый экземпляр структуры владел всеми своими данными и чтобы эти данные оставались действительными, пока действительна вся структура.

Структуры могут хранить и ссылки на данные, которыми владеет кто-то другой, но для этого нужны времена жизни (lifetimes) — о них мы поговорим в главе 10. Времена жизни гарантируют, что данные, на которые ссылается структура, действительны, пока действительна сама структура. Допустим, вы пытаетесь сохранить в структуре ссылку, не указав времена жизни, — как в следующем коде в src/main.rs. Так не выйдет:

Файл: 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.