Зачем склеивать несколько значений в одно
Представьте, что вам нужно вернуть из вычисления сразу два числа — например, частное и остаток от деления. Можно завести под это отдельный именованный тип, но заводить целый новый тип ради одного места в коде, где он используется, — перебор. Хочется чего-то попроще: временно связать два-три значения вместе, передать их одним куском, а потом разобрать обратно.
Именно для этого в Swift есть кортеж (tuple) — способ упаковать несколько значений, возможно разных типов, в одно составное значение, не объявляя для него отдельный тип. Кортеж существует ровно там, где вы его создали, и не требует никакого описания заранее.
Тип кортежа и обращение по номеру
Кортеж записывается как список типов в круглых скобках, а значение — как список значений в круглых скобках:
let point: (Int, Int) = (3, 7)
print(point.0)
print(point.1)
Тип (Int, Int) означает «кортеж из двух Int по порядку». Переменная point хранит сразу оба числа. Обратиться к отдельному элементу можно через точку и номер позиции, считая от нуля: point.0 — первый элемент, point.1 — второй. Аннотацию типа можно и опустить — Swift выведет её из значения справа, как и для обычных переменных:
let size = (1920, 1080)
print(size.0, size.1) // 1920 1080
Элементы кортежа не обязаны быть одного типа. Кортеж (Int, String) хранит число и строку вместе — это ровно то, что нужно, когда результат состоит из «числа и подписи к нему»:
let httpStatus = (404, "Not Found")
print(httpStatus.0)
print(httpStatus.1)
Именованные элементы
Обращение по номеру быстро становится неудобным: глядя на .0 и .1, невозможно догадаться, что там лежит. Поэтому элементам кортежа можно дать имена прямо в объявлении:
let status = (code: 404, message: "Not Found")
print(status.code)
print(status.message)
Имена code и message — часть значения, а не отдельного типа: полный тип этого кортежа записывается как (code: Int, message: String). Обращаться к элементам теперь можно по имени — status.code, status.message — это гораздо понятнее, чем .0 и .1. Доступ по номеру при этом никуда не девается и продолжает работать, но с именованными элементами им обычно уже не пользуются.
Имена нужно указывать сразу у всех элементов кортежа или ни у одного — смешивать «этот с именем, а этот без» нельзя.
Деструктуризация: разбираем кортеж на отдельные переменные
Кортеж редко хранят долго — обычно его тут же разбирают на отдельные именованные переменные, чтобы дальше работать с каждым значением само по себе. Это называется деструктуризацией и делается прямо при присваивании:
let (code, message) = (404, "Not Found")
print(code)
print(message)
Здесь слева не один кортеж, а сразу два новых имени — code и message, каждое получает свою часть кортежа справа. Дальше в коде это уже две обычные независимые константы, а не элементы одного составного значения.
Если из кортежа нужна только часть значений, ненужные элементы заменяют символом подчёркивания _ — он означает «это значение мне не нужно, даже не создавай для него переменную»:
let (_, message) = (404, "Not Found")
print(message) // code потерян осознанно, доступа к нему больше нет
Главное применение — несколько результатов из функции
Обычная функция возвращает ровно одно значение. Если результата логически два или три и заводить под них отдельный тип не хочется, кортеж — самый простой выход: он же и есть одно значение, просто составное.
func divide(_ dividend: Int, _ divisor: Int) -> (quotient: Int, remainder: Int) {
return (dividend / divisor, dividend % divisor)
}
let result = divide(17, 5)
print(result.quotient)
print(result.remainder)
Тип результата функции divide — (quotient: Int, remainder: Int), кортеж с именованными элементами. Внутри функции создаётся кортеж-значение (dividend / divisor, dividend % divisor) и возвращается наружу целиком. У вызывающего кода сразу оказываются под рукой оба числа под понятными именами.
Результат вызова функции тоже можно сразу деструктурировать, минуя промежуточное имя для целого кортежа:
let (quotient, remainder) = divide(17, 5)
print(quotient)
print(remainder)
Сравнение кортежей на равенство
Кортежи из одинакового набора типов можно сравнивать оператором == — сравнение идёт поэлементно: кортежи равны, только если равны все элементы на своих местах.
let a = (1, "ok")
let b = (1, "ok")
let c = (1, "fail")
print(a == b) // true
print(a == c) // false
Это работает, только если каждый элемент кортежа сам по себе поддерживает сравнение через == — то есть числа, строки, булевы значения и всё остальное, что вы уже сравнивали раньше. Сравнить между собой можно только кортежи одинаковой длины с элементами одинаковых типов на соответствующих местах: (1, "ok") и (1, 2) сравнить нельзя, это разные типы кортежа.
Где кортеж заканчивается и начинается своя структура
Кортеж хорош, пока значений немного (обычно два-три) и живут они внутри одной функции или рядом с местом, где родились: посчитали, тут же разобрали, использовали. Как только с этим же набором значений начинает происходить что-то большее — вы передаёте его в несколько разных мест кода, хотите дать ему собственные методы, или элементов становится много и по номерам с именами уже сложно уследить, что где — это сигнал, что на самом деле у вас есть модель данных, а не временная связка. Для модели данных в Swift существует собственный тип — структура (struct); как её объявлять, разберём отдельно позже. Пока достаточно запомнить правило: кортеж — для короткой временной упаковки значений, структура — когда у данных появляется собственный смысл и им предстоит долгая жизнь в программе.
Частые ошибки
- Пытаться обратиться к элементу по имени, если кортеж объявлен без имён — компилятор не найдёт такого свойства, потому что у безымянного кортежа есть только
.0,.1и так далее. - Указывать имена только у части элементов кортежа — Swift требует либо у всех, либо ни у одного.
- Забывать скобки при возврате нескольких значений из функции: тип результата — это один кортежный тип
(Int, String), а не «два возвращаемых значения через запятую». - Пытаться сравнить оператором
==кортежи разной длины или с элементами разных типов на одинаковых позициях — компилятор такое сравнение не пропустит, это разные типы. - Использовать кортеж как замену полноценной модели данных: как только значений становится много или их начинают передавать по всей программе, разрозненные
.0,.1,.2читать намного тяжелее, чем несколько понятных именованных свойств структуры.
Резюме
- Кортеж упаковывает несколько значений разных типов в одно составное значение без объявления отдельного типа:
(Int, String). - Элементы без имён доступны по номеру —
.0,.1; элементам можно дать имена при объявлении и обращаться к ним по имени —.code,.message. - Кортеж можно разобрать на отдельные переменные прямо при присваивании — деструктуризация; ненужный элемент заменяют символом
_. - Главное практическое применение — вернуть из функции сразу несколько значений одним типом-кортежем.
- Кортежи одинакового типа сравниваются оператором
==поэлементно. - Кортеж — для двух-трёх значений с короткой жизнью внутри одной функции; для полноценной модели данных нужна структура.
Проверьте себя
3 вопроса
Частное и остаток
Напишите функцию divide(_ dividend: Int, _ divisor: Int) -> (quotient: Int, remainder: Int), которая возвращает кортеж с именованными элементами: quotient — целая часть деления dividend на divisor, remainder — остаток от этого деления.
Используйте целочисленное деление / и оператор остатка %.
Минимум и максимум
Напишите функцию minAndMax(_ a: Int, _ b: Int, _ c: Int) -> (min: Int, max: Int), которая принимает три целых числа и возвращает именованный кортеж: min — наименьшее из трёх чисел, max — наибольшее из трёх чисел.
В стандартной библиотеке Swift уже есть готовые функции min(_:_:_:) и max(_:_:_:), которые принимают несколько чисел и сами находят наименьшее или наибольшее среди них — писать сравнение вручную не нужно, достаточно передать туда a, b и c.