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

Кортежи в Rust

Кортеж (tuple) объединяет несколько значений — возможно, разных типов — в одно. Это быстрый способ собрать вместе связанные данные, не объявляя для них целый новый тип.

fn main() {
let person: (&str, i32, bool) = ("Алиса", 30, true);
println!("{:?}", person);
}

Вывод:

("Алиса", 30, true)
▶ Попробуй сам

Обрати внимание: в одном кортеже смешаны &str, i32 и bool. Массив так не умеет — все его элементы должны быть одного типа.

Чтобы вывести кортеж целиком, используй {:?} (отладочный формат), а не {}. Обычный {} для кортежей не работает.

Используй .0, .1, .2 и так далее — позиции в кортеже считаются с нуля:

fn main() {
let person = ("Алиса", 30, true);
println!("Имя: {}", person.0);
println!("Возраст: {}", person.1);
println!("Активна: {}", person.2);
}

Вывод:

Имя: Алиса
Возраст: 30
Активна: true
▶ Попробуй сам

Позиция должна быть обычным числом, записанным прямо в коде. Переменную вроде person.i использовать нельзя: на каждой позиции может лежать значение своего типа, а Rust должен знать тип ещё при компиляции.

Все значения можно разложить по отдельным переменным одной строкой:

fn main() {
let person = ("Алиса", 30, true);
let (name, age, is_active) = person;
println!("{name}, {age} лет, активна: {is_active}");
}

Вывод:

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

Часто это понятнее, чем раз за разом писать .0, .1, .2, особенно когда полей в кортеже больше двух-трёх.

Возвращаем из функции несколько значений

Заголовок раздела «Возвращаем из функции несколько значений»

Кортежи — естественный способ вернуть больше одного значения, ведь функция может объявить только один тип результата:

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("минимум: {smallest}, максимум: {largest}");
}

Вывод:

минимум: 1, максимум: 9
▶ Попробуй сам

Пока не обращай внимания на &[i32], .iter() и .unwrap() — о них рассказано в уроках Срезы, Итераторы и Option. Здесь важно другое: тип результата (i32, i32) и то, как main сразу раскладывает его на две переменные.

Если кортеж объявлен с mut, его поля можно менять по одному через те же позиции .0, .1. Значения меняться могут, а форма — нет: у кортежа всегда остаются то же число полей и те же типы, с которыми он появился.

Деструктуризация работает и в обратную сторону: можно присваивать в уже существующие переменные. Так получается аккуратный обмен значений в одну строку, без временной переменной. Если нужны не все поля, напиши _ вместо ненужных:

fn main() {
let mut point = (3, 4);
point.0 += 10; // меняем одно поле на месте
println!("точка: {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // меняем значения местами
println!("a = {a}, b = {b}");
let (x, _) = point; // берём x, второе поле пропускаем
println!("x = {x}");
}

Вывод:

точка: (13, 4)
a = 2, b = 1
x = 13
▶ Попробуй сам

Обмен работает потому, что Rust сначала собирает правую часть, (b, a), и только потом раскладывает её в a и b. Гораздо больше возможностей _ и образцов — в уроке Сопоставление с образцом.

На самом деле ты уже встречал кортеж, сам того не замечая. () называется единичным типом (unit type), и именно его возвращает функция, которая не возвращает ничего осмысленного. Это неявный тип результата fn main().

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

Напиши функцию sum_and_product(a: i32, b: i32) -> (i32, i32), которая возвращает и сумму, и произведение двух чисел. В main вызови её с 5 и 6 и деструктурируй результат. Подсказка: последняя строка функции — сам кортеж (a + b, a * b), без точки с запятой.

Показать решение
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("сумма = {sum}, произведение = {product}");
}

Вывод:

сумма = 11, произведение = 30
▶ Попробуй сам

Прямоугольник можно хранить как кортеж из двух точек, где каждая точка — тоже кортеж: let rect = ((1, 2), (5, 5));. Вычисли его ширину, высоту и площадь. Подсказка: кортежи можно вкладывать друг в друга, и rect.1.0 значит «первое поле второй точки».

Показать решение
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("ширина {width}, высота {height}, площадь {}", width * height);
}

Вывод:

ширина 4, высота 3, площадь 12
▶ Попробуй сам

Можно сразу деструктурировать обе точки: let ((x1, y1), (x2, y2)) = rect;. Печатать дольше, зато x2 - x1 читается легче, чем rect.1.0 - rect.0.0. Когда точки начинают громоздиться вот так, это знак, что структура с именованными полями была бы понятнее.

  • Кортеж объединяет значения, возможно, разных типов: (name, age, is_active).
  • К полям обращаются через .0, .1, .2 или разом раскладывают их деструктуризацией.
  • Кортежи — естественный способ вернуть из функции несколько значений.
  • Поля mut-кортежа можно менять, а его размер и типы — нет. (a, b) = (b, a); меняет два значения местами.
  • Для именованных группировок, которые используются много раз, бери структуру.

Проверь себя

1. Что может хранить кортеж, чего не может массив?

2. Как обратиться ко второму элементу кортежа person?

3. Чему равны a и b после let mut a = 1; let mut b = 2; (a, b) = (b, a);?

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