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

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

Чтобы определить структуры, мы вводим ключевое слово struct и название структуры. Название должно описывать значение частей данных, сгруппированных вместе. Далее, в фигурных скобках для каждого поля поочерёдно определяются имя и тип. Листинг 5-1 показывает структуру, которая хранит информацию об учётной записи пользователя:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

После определения структуры можно создавать её экземпляр, назначая определённое значение с соответствующим типом данных каждому полю. Чтобы создать экземпляр структуры, мы указываем имя структуры, а затем добавляем фигурные скобки и включаем в них пары ключ: значение, где ключами являются имена полей, а значениями являются данные, которые мы хотим сохранить в полях. Нет необходимости чётко следовать порядку объявления полей в описании структуры (но это всё-таки желательно для удобства чтения). Другими словами, объявление структуры — это нечто вроде схемы нашей группы данных, в то время как экземпляр структуры использует эту схему, заполняя её определёнными данными. Например, пользователя можно объявить так, как показано в Листинге 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,
    };
}

Чтобы получить конкретное значение из структуры, мы обращаемся к нему по имени поля через точку. Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы пишем 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");
}

Стоит отметить, что весь экземпляр структуры должен быть изменяемым; 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"),
    );
}

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

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

Так как в Листинге 5-4 имена параметров функции и полей структуры одни и те же, можно использовать синтаксис сокращённой инициализации поля, чтобы определить build_user так, чтобы он не содержал повторений 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"),
    );
}

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

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

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

В Листинге 5-6 показано, как в user2 записывается новый экземпляр User без синтаксиса обновления. Мы задаём новое значение для 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,
    };
}

Используя синтаксис обновления структуры, можно получить тот же эффект, используя меньше кода, как показано в Листинге 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
    };
}

Код в Листинге 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, поэтому они ведут себя так, как мы обсуждали в разделе "Данные, размещающиеся только на стеке, всегда копируются". Однако, в нашем примере мы всё ещё можем использовать запись user1.email, поскольку значение этого поля не было перемещено.

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

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

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

Unit-подобные структуры

Также можно определять структуры, не имеющие полей! Они называются unit-подобными структурами, поскольку ведут себя аналогично (), unit, о котором мы говорили в разделе ["Тип кортежа"] (ch03-02-data-types.html#Тип-кортежа). Unit-подобные структуры могут быть полезны, когда требуется реализовать трейт для некоторого типа, но у вас нет данных, которые нужно было бы хранить в самом типе. Мы обсудим трейты в Главе 10. Вот пример объявления и создания экземпляра unit-структуры с именем AlwaysEqual:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

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

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

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

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

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.