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

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

Structs are similar to tuples, discussed in “The Tuple Type” section, in that both hold multiple related values. Like tuples, the pieces of a struct can be different types. Unlike with tuples, in a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: You don’t have to rely on the order of the data to specify or access the values of an instance.

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

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

fn main() {}
Listing 5-1: A User struct definition

To use a struct after we’ve defined it, we create an instance of that struct by specifying concrete values for each of the fields. We create an instance by stating the name of the struct and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the data we want to store in those fields. We don’t have to specify the fields in the same order in which we declared them in the struct. In other words, the struct definition is like a general template for the type, and instances fill in that template with particular data to create values of the type. For example, we can declare a particular user as shown in Listing 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: Creating an instance of the User struct

Чтобы получить конкретное значение из структуры, мы обращаемся к нему по имени поля через точку. Например, чтобы получить доступ к адресу электронной почты этого пользователя, мы пишем 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: Changing the value in the email field of a User instance

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

Listing 5-4 shows a build_user function that returns a User instance with the given email and username. The active field gets the value true, and the sign_in_count gets a value of 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: A build_user function that takes an email and username and returns a User instance

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

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

Because the parameter names and the struct field names are exactly the same in Listing 5-4, we can use the field init shorthand syntax to rewrite build_user so that it behaves exactly the same but doesn’t have the repetition of username and email, as shown in Listing 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: A build_user function that uses field init shorthand because the username and email parameters have the same name as struct fields

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

Creating Instances with Struct Update Syntax

It’s often useful to create a new instance of a struct that includes most of the values from another instance of the same type, but changes some of them. You can do this using struct update syntax.

First, in Listing 5-6 we show how to create a new User instance in user2 in the regular way, without the update syntax. We set a new value for email but otherwise use the same values from user1 that we created in Listing 5-2.

Filename: 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,
    };
}
Listing 5-6: Creating a new User instance using all but one of the values from user1

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

Filename: 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
    };
}
Listing 5-7: Using struct update syntax to set a new email value for a User instance but to use the rest of the values from user1

Код в Листинге 5-7 тоже создаёт экземпляр user2, который имеет другое значение email, но не меняет значения полей username, active и sign_in_count из user1. Конструкция ..user1 должна стоять последней; она указывает на получение значений всех оставшихся полей из соответствующих полей в user1, но перед этим мы можем указать свои значения для любых полей в любом порядке, независимо от порядка полей в определении структуры.

Note that the struct update syntax uses = like an assignment; this is because it moves the data, just as we saw in the “Variables and Data Interacting with Move” section. In this example, we can no longer use user1 after creating user2 because the String in the username field of user1 was moved into user2. If we had given user2 new String values for both email and username, and thus only used the active and sign_in_count values from user1, then user1 would still be valid after creating user2. Both active and sign_in_count are types that implement the Copy trait, so the behavior we discussed in the “Stack-Only Data: Copy” section would apply. We can also still use user1.email in this example, because its value was not moved out of user1.

Creating Different Types with Tuple Structs

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);
}

Note that the black and origin values are different types because they’re instances of different tuple structs. Each struct you define is its own type, even though the fields within the struct might have the same types. For example, a function that takes a parameter of type Color cannot take a Point as an argument, even though both types are made up of three i32 values. Otherwise, tuple struct instances are similar to tuples in that you can destructure them into their individual pieces, and you can use a . followed by the index to access an individual value. Unlike tuples, tuple structs require you to name the type of the struct when you destructure them. For example, we would write let Point(x, y, z) = origin; to destructure the values in the origin point into variables named x, y, and z.

Defining Unit-Like Structs

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

Filename: src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

To define AlwaysEqual, we use the struct keyword, the name we want, and then a semicolon. No need for curly brackets or parentheses! Then, we can get an instance of AlwaysEqual in the subject variable in a similar way: using the name we defined, without any curly brackets or parentheses. Imagine that later we’ll implement behavior for this type such that every instance of AlwaysEqual is always equal to every instance of any other type, perhaps to have a known result for testing purposes. We wouldn’t need any data to implement that behavior! You’ll see in Chapter 10 how to define traits and implement them on any type, including unit-like structs.

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

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

It’s also possible for structs to store references to data owned by something else, but to do so requires the use of lifetimes, a Rust feature that we’ll discuss in Chapter 10. Lifetimes ensure that the data referenced by a struct is valid for as long as the struct is. Let’s say you try to store a reference in a struct without specifying lifetimes, like the following in src/main.rs; this won’t work:

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

In Chapter 10, we’ll discuss how to fix these errors so that you can store references in structs, but for now, we’ll fix errors like these using owned types like String instead of references like &str.