Перейти к содержимому

Структуры в Rust

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

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Алиса"),
age: 30,
};
println!("{}: {} лет", alice.name, alice.age);
}

Вывод:

Алиса: 30 лет
▶ Попробуй сам

Сравни с кортежем ("Алиса", 30): у структуры alice.name и alice.age говорят сами за себя. Не нужно помнить, что поле 0 — это имя, а поле 1 — возраст.

Как и переменные, экземпляры структур по умолчанию неизменяемые. Добавь mut, чтобы менять поле после создания:

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Алиса"),
age: 30,
};
alice.age += 1;
println!("{} теперь {} лет", alice.name, alice.age);
}
▶ Попробуй сам

К структуре можно привязать функции с помощью блока impl. Внутри него &self означает тот самый экземпляр, у которого вызвали метод:

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("Привет, я {}, мне {} лет", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Алиса"), age: 30 };
alice.greet();
}

Вывод:

Привет, я Алиса, мне 30 лет
▶ Попробуй сам

Частый приём — функция new внутри impl, которая собирает экземпляр за тебя. Тогда тому, кто её вызывает, не нужно вручную перечислять все поля:

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Боб", 25);
println!("{}: {} лет", bob.name, bob.age);
}
▶ Попробуй сам

Обрати внимание на одинокое age в Person { name: name.to_string(), age }. Если у переменной точно такое же имя, как у поля, можно не писать age: age, а написать просто age. В Rust это называется сокращённой инициализацией полей (field init shorthand).

Попробуй написать println!("{:?}", alice) для структуры из примеров выше — код не скомпилируется. Rust не знает, как выводить твой собственный тип для отладки, пока ты его не попросишь. Добавь #[derive(Debug)] над структурой, и это умение появится бесплатно:

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Алиса"), age: 30 };
println!("{:?}", alice);
}

Вывод:

Person { name: "Алиса", age: 30 }
▶ Попробуй сам

#[derive(...)] — это способ Rust автоматически сгенерировать для типа типичную функциональность. Ты постоянно будешь видеть, как так добавляют Debug, а позже Clone и другие трейты.

Создай структуру Rectangle с полями width и height (u32). Дай ей два метода: area, который возвращает площадь, и is_square, который возвращает true, когда стороны равны. Подсказка: оба метода только читают поля, поэтому принимают &self.

Показать решение
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("широкий: площадь {}, квадрат? {}", wide.area(), wide.is_square());
println!("квадрат: площадь {}, квадрат? {}", square.area(), square.is_square());
}

Вывод:

широкий: площадь 24, квадрат? false
квадрат: площадь 25, квадрат? true
▶ Попробуй сам

Создай структуру Account с полями owner (String) и balance — баланс в копейках (u32). Напиши Account::new("Аня"), которая начинает с баланса 0. Затем положи на счёт 50 и 25 копеек, меняя поле, и выведи счёт двумя способами: обычным предложением и через {:?}. Подсказка: чтобы менять account.balance, переменная должна быть let mut, а для {:?} нужен #[derive(Debug)].

Показать решение
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Аня");
account.balance += 50;
account.balance += 25;
println!("У владельца {} на счёте {} коп.", account.owner, account.balance);
println!("{:?}", account);
}

Вывод:

У владельца Аня на счёте 75 коп.
Account { owner: "Аня", balance: 75 }
▶ Попробуй сам

new сама ставит balance: 0, так что вызывающий код не может об этом забыть. Во многом поэтому функции-конструкторы так популярны.

Положи три значения Person в вектор: Аня (34), Бен (16) и Чэнь (52). Выведи имена всех, кому 18 или больше, а затем — кто самый старший. Подсказка: перебирай людей циклом for person in &people и запоминай самого старшего на данный момент через let mut oldest = &people[0];.

Показать решение
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Аня"), age: 34 },
Person { name: String::from("Бен"), age: 16 },
Person { name: String::from("Чэнь"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} — взрослый человек", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("Самый старший: {} ({})", oldest.name, oldest.age);
}

Вывод:

Аня — взрослый человек
Чэнь — взрослый человек
Самый старший: Чэнь (52)
▶ Попробуй сам

oldest — это ссылка (&Person) на элемент вектора, а не копия, так что ничего не клонируется. Каждый раз, когда находится кто-то старше, мы просто перенаправляем oldest на него.

  • Структура собирает именованные поля в один тип: struct Person { name: String, age: u32 }.
  • Экземпляр создаётся так: Person { name: ..., age: ... }. Отметь его mut, чтобы потом менять поля.
  • Поведение добавляется блоком impl; &self — это экземпляр, у которого вызвали метод.
  • Добавь #[derive(Debug)], чтобы структуру можно было вывести через {:?}.

Проверь себя

1. Главное преимущество структуры перед кортежем?

2. Что даёт #[derive(Debug)] у структуры?

3. Что делает одинокое age в Person { name: name.to_string(), age }?

Результат: 0 / 3