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.

Чтобы получить конкретное значение из структуры, мы используем точечную нотацию. Например, чтобы получить адрес электронной почты этого пользователя, мы используем user1.email. Если экземпляр изменяемый, мы можем изменить значение, используя точечную нотацию и присваивание конкретному полю. Листинг 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.

Листинг 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, но мы можем выбрать установку значений для любого количества полей в любом порядке, независимо от порядка полей в определении структуры.

Обратите внимание, что синтаксис обновления структур использует =, как присваивание; это происходит потому, что он перемещает данные, как мы видели в разделе “Что такое владение?”. В этом примере после создания user2 user1 частично становится недействительным, потому что String в поле username user1 была перемещена в user2. Если бы мы дали user2 новые значения String как для email, так и для username, и, таким образом, использовали только значения active и sign_in_count из user1, то user1 оставался бы полностью действительным после создания user2. Типы active и sign_in_count — это типы, которые реализуют типаж Copy, поэтому поведение, которое мы обсудили в разделе “Копирование против перемещения из коллекции”, применилось бы.

Использование кортежных структур без именованных полей для создания разных типов

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

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

Filename: src/main.rs

Обратите внимание, что значения black и origin имеют разные типы, потому что они являются экземплярами разных кортежных структур. Каждая определяемая вами структура является своим собственным типом, даже если поля внутри структуры могут иметь одинаковые типы. Например, функция, которая принимает параметр типа Color, не может принять Point в качестве аргумента, даже если оба типа состоят из трёх значений i32. В остальном экземпляры кортежных структур похожи на кортежи в том смысле, что вы можете деструктурировать их на отдельные части, и вы можете использовать . followed by the index to access an individual value. В отличие от кортежей, кортежные структуры требуют, чтобы вы указывали имя типа структуры при их деструктуризации. Например, мы бы написали let Point(x, y, z) = origin;, чтобы деструктурировать значения в точке origin в переменные с именами x, y и z.

Структуры, подобные единичному типу, без каких-либо полей

Вы также можете определить структуры, которые не имеют никаких полей! Они называются структурами, подобными единичному типу, потому что они ведут себя аналогично (), единичному типу, который мы упоминали в разделе “Тип кортежа”. Структуры, подобные единичному типу, могут быть полезны, когда вам нужно реализовать типаж для некоторого типа, но у вас нет данных, которые вы хотите хранить в самом типе. Мы обсудим типажи в Главе 10. Вот пример объявления и создания экземпляра структуры, подобной единичному типу, с именем AlwaysEqual:

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

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

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

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

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.

Заимствование полей структуры

Подобно нашему обсуждению в “Разные поля кортежа”, проверка заимствований Rust будет отслеживать разрешения владения как на уровне структуры, так и на уровне поля. Например, если мы заимствуем поле x структуры Point, то и p, и p.x временно теряют свои разрешения (но не p.y):

В результате, если мы попытаемся использовать p, пока p.x заимствован изменяемо, как здесь:

Тогда компилятор отклонит нашу программу со следующей ошибкой:

error[E0502]: cannot borrow `p` as immutable because it is also borrowed as mutable
  --> test.rs:10:17
   |
9  |     let x = &mut p.x;
   |             -------- mutable borrow occurs here
10 |     print_point(&p);
   |                 ^^ immutable borrow occurs here
11 |     *x += 1;
   |     ------- mutable borrow later used here

В более общем случае, если вы столкнётесь с ошибкой владения, связанной со структурой, вам следует рассмотреть, какие поля вашей структуры должны заимствоваться с какими разрешениями. Но имейте в виду ограничения проверки заимствований, поскольку Rust иногда может предполагать, что заимствовано больше полей, чем есть на самом деле.