$ sudo teach IT

Зачем склеивать несколько значений в одно

Представьте, что вам нужно вернуть из вычисления сразу два числа — например, частное и остаток от деления. Можно завести под это отдельный именованный тип, но заводить целый новый тип ради одного места в коде, где он используется, — перебор. Хочется чего-то попроще: временно связать два-три значения вместе, передать их одним куском, а потом разобрать обратно.

Именно для этого в 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.