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

Внешне структуры похожи на кортежи, рассмотренные в Главе 3. Также как кортежи, структуры могут содержать разные типы данных. Но в отличии от кортежей, вы именуете части данных так, чтобы было ясно, что эти имена означают. Поэтому структуры более удобны для создания новых типов данных, так как нет необходимости запоминать порядковый номер какого-либо значения внутри экземпляра структуры.

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

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

fn main() {}

Листинг 5-1: определение структуры User

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

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

fn main() {
    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
}

Листинг 5-2: создание экземпляра структуры User

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

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

fn main() {
    let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        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.

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

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        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 так, чтобы он работал точно также, но не содержал повторений для email и username, как в листинге 5-5.

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

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Листинг 5-5: функция build_user использует сокращённую инициализацию полей, потому что её входные параметры email и username имеют имена аналогичные именам полей структуры

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

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

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

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

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

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"),
        username: String::from("anotherusername567"),
        active: user1.active,
        sign_in_count: user1.sign_in_count,
    };
}

Листинг 5-6: создание экземпляра User с присвоением некоторым полям значений из user1

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

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

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"),
        username: String::from("anotherusername567"),
        ..user1
    };
}

Листинг 5-7: используем синтаксис обновления структур для установки значений email и username экземпляра User, но для остальных значений берём данные из полей экземпляра переменной user1

Код в листинге Listing 5-7 также создаёт экземпляр переменной user2, который имеет отличные от user1 значения полей email и username, но те же самые значения полей active и sign_in_count.

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

Можно определять структуры очень напоминающие кортежи (такие структуры называют кортежными структурами, tuple structs). Кортежные структуры имеют дополнительный смысл из-за имени в объявлении, но не имеют имён, ассоциированных с их полями, у них есть только типы полей. Кортежные структуры удобны, когда нужно дать всему кортежу имя и сделать его отличным от других кортежей, при этом именование полей как у обычной структуры будет подробным или избыточным.

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

fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Обратите внимание, что переменные black и origin разного типа, потому что они являются экземплярами разных кортежных структур. Каждая определяемая структура является собственным типом, не смотря на то, что поля внутри структуры имеют одинаковые типы. Например, функция, принимающая параметром тип Color, не может принять аргумент типа Point, не смотря на то, что оба типа состоят из трёх значений i32. Тем не менее, экземпляры кортежных структур ведут себя как кортежи: их можно разделять на отдельные части, использовать . за которой идёт индекс для доступа к отдельному значению и т.д.

Единично-подобные структуры: структуры без полей

Можно также определять структуры вообще без полей! Они называются unit-like, единично-подобные структуры, потому что ведут себя подобно единичному типу (). Единично-подобные структуры могут быть полезны в ситуации, в которой нужно реализовать типаж (trait) для некоторого типа, но нет никаких данных для сохранения в самом типе. Мы обсудим типажи в главе 10.

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

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

Структуры могут хранить ссылки на данные, которыми владеет кто-то другой, но для этого требуется использование времени жизни (lifetimes) — функции Rust, которую мы обсудим в главе 10. Время жизни гарантирует, что данные, на которые ссылается структура, действительны, пока существует сама структура. Если вы попытаетесь сохранить ссылку в структуре без указания времени жизни, как тут, то это не сработает:

Файл : src/main.rs

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

 fn main() {
     let user1 = User {
         email: "someone@example.com",
         username: "someusername123",
         active: true,
         sign_in_count: 1,
     };
 }

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

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct User<'a> {
2 |     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 | struct User<'a> {
2 |     username: &str,
3 |     email: &'a str,
  |

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs`

To learn more, run the command again with --verbose.

В Главе 10 мы обсудим, как исправить такие ошибки сохранения ссылок в структурах. Но сейчас мы просто забудем об этих ошибках и будем использовать типы с владением данными, по типу String вместо ссылочных типов, таких как &str.