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 и задаём имя всей структуры. Имя структуры должно описывать смысл частей данных, сгруппированных вместе. Затем внутри фигурных скобок мы определяем имена и типы частей данных, которые называем полями. Например, листинг 5-1 показывает структуру, которая хранит информацию об учётной записи пользователя.

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

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

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

Filename: 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,
    };
}
Listing 5-2: Создание экземпляра структуры User

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

Filename: 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");
}
Listing 5-3: Изменение значения поля email экземпляра User

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

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

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

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

Использование сокращённой инициализации полей

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

Filename: 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"),
    );
}
Listing 5-5: Функция build_user, использующая сокращённую инициализацию полей, потому что параметры username и email имеют те же имена, что и поля структуры

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

Создание экземпляров с помощью синтаксиса обновления структуры

Часто бывает полезно создать новый экземпляр структуры, который включает большинство значений из другого экземпляра того же типа, но изменяет некоторые из них. Это можно сделать с помощью синтаксиса обновления структуры.

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    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,
    };
}
Listing 5-6: Создание нового экземпляра User с использованием всех значений из user1, кроме одного

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

Filename: src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    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
    };
}
Listing 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, а значит, использовали бы из user1 только значения active и sign_in_count, тогда user1 оставался бы действительным после создания user2. И active, и sign_in_count имеют типы, реализующие трейт Copy, поэтому применялось бы поведение, которое мы обсуждали в разделе «Данные только в стеке: Copy». В этом примере мы также всё ещё можем использовать user1.email, потому что его значение не было перемещено из user1.

Создание разных типов с помощью кортежных структур

Rust также поддерживает структуры, похожие на кортежи, которые называются кортежными структурами. Кортежные структуры имеют дополнительный смысл, который даёт имя структуры, но у них нет имён, связанных с полями; вместо этого у них есть только типы полей. Кортежные структуры полезны, когда вы хотите дать имя всему кортежу и сделать этот кортеж отличным типом от других кортежей, а именование каждого поля, как в обычной структуре, было бы многословным или избыточным.

Чтобы определить кортежную структуру, начните с ключевого слова struct и имени структуры, после которого идут типы в кортеже. Например, здесь мы определяем и используем две кортежные структуры с именами Color и Point:

Filename: 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. В остальном экземпляры кортежных структур похожи на кортежи: их можно деструктурировать на отдельные части, а для доступа к отдельному значению можно использовать . с последующим индексом. В отличие от кортежей, кортежные структуры требуют указывать имя типа структуры при деструктурировании. Например, мы написали бы let Point(x, y, z) = origin;, чтобы деструктурировать значения точки origin в переменные с именами x, y и z.

Определение unit-like структур

Вы также можете определять структуры, у которых вообще нет полей! Они называются unit-like структурами, потому что ведут себя похоже на () — unit-тип, о котором мы упоминали в разделе «Тип кортежа». Unit-like структуры могут быть полезны, когда нужно реализовать трейт для некоторого типа, но у вас нет данных, которые вы хотите хранить в самом типе. Мы обсудим трейты в Главе 10. Вот пример объявления и создания экземпляра unit-структуры с именем AlwaysEqual:

Filename: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Чтобы определить AlwaysEqual, мы используем ключевое слово struct, нужное нам имя, а затем точку с запятой. Фигурные скобки или круглые скобки не нужны! Затем похожим образом мы можем получить экземпляр AlwaysEqual в переменной subject: используя определённое нами имя без фигурных или круглых скобок. Представьте, что позже мы реализуем поведение для этого типа так, что каждый экземпляр AlwaysEqual всегда равен каждому экземпляру любого другого типа, возможно, чтобы иметь известный результат для целей тестирования. Для реализации такого поведения нам не понадобились бы никакие данные! В Главе 10 вы увидите, как определять трейты и реализовывать их для любого типа, включая unit-like структуры.

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

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

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

Filename: 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.