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

Параметры функций и читаемость API в Swift

Разработчику Архитектору

Идея "вызов как фраза"

Напоминание: общие понятия функций в программе — функции в коде.

В Swift имя функции при вызове часто складывается из меток параметров. Это не декорация: метки входят в идентификатор метода и делают код похожим на осмысленное предложение.

func move(from start: Point, to end: Point) { /* ... */ }
move(from: origin, to: destination)

Разбор:

  • func move(...) объявляет функцию с двумя параметрами и внешними метками from и to.
  • Внутренние имена start и end используются внутри тела функции, повышая читаемость реализации.
  • Вызов move(from:origin, to:destination) читается как фраза и явно показывает роль каждого аргумента.
  • Такой стиль снижает риск перепутать порядок однотипных параметров.

Сравните с вызовом без меток — move(origin, destination) — короче, но при шести параметрах читаемость падает. Apple API (UIKit, SwiftUI, Foundation) последовательно используют метки — привыкание к ним облегчает чтение документации и чужого кода.


Внешнее и внутреннее имя

У каждого параметра (кроме особых случаев) могут быть:

  • внешнее имя — видно при вызове;
  • внутреннее имя — используется в теле функции.
func greet(person name: String, times count: Int) {
for _ in 0..<count {
print("Hello, \(name)")
}
}
greet(person: "Мария", times: 3)

Разбор:

  • person/times — внешние метки для вызова, name/count — внутренние имена в теле функции.
  • Цикл for _ in 0..<count выполняет блок ровно count раз; _ означает, что индекс итерации не нужен.
  • Интерполяция \(name) вставляет значение переменной в строку без ручной конкатенации.
  • Вызов greet(person:..., times:...) демонстрирует "самодокументируемый" API за счёт меток.

Здесь person и times — внешние, name и count — внутренние.

Если внешнее имя не указано, по умолчанию оно совпадает с внутренним. Первый параметр метода экземпляра (instance method) по соглашению не имеет внешней метки, если не задана явно — поэтому array.append(element) выглядит естественно.


Подчёркивание _ — скрыть метку

Когда метка при вызове только мешает:

func add(_ a: Int, _ b: Int) -> Int { a + b }
add(2, 3)

Разбор:

  • Символ _ убирает внешние метки параметров, делая вызов короче.
  • -> Int задаёт тип возвращаемого значения.
  • Тело { a + b } — сокращённая форма функции из одного выражения.
  • Подход уместен для компактных утилитарных операций, где смысл аргументов очевиден.

Используйте _ осознанно: для "математических" функций и оператороподобных методов. Для публичного API библиотеки чаще оставляют метки — так проще искать вызовы и понимать аргументы.


Значения по умолчанию

Параметр с default можно не передавать:

func connect(host: String, port: Int = 443, useTLS: Bool = true) { /* ... */ }
connect(host: "api.example.com")
connect(host: "legacy.example.com", port: 8080, useTLS: false)

Разбор:

  • port и useTLS имеют значения по умолчанию, поэтому их можно опускать в вызове.
  • Первый вызов использует дефолты (443 и true) и выглядит как "безопасный стандарт".
  • Второй вызов явно переопределяет параметры для нестандартного хоста.
  • Такой дизайн API сохраняет и краткость типичного вызова, и гибкость для редких сценариев.

Правила:

  • параметры с default обычно идут после обязательных;
  • при вызове пропущенные аргументы подставляются из объявления;
  • default вычисляется в момент вызова (важно для Date(), случайных значений).

Вариадические параметры

Функция может принять произвольное число однотипных аргументов через ...:

func sum(_ numbers: Int...) -> Int {
numbers.reduce(0, +)
}
sum(1, 2, 3, 4)

Разбор:

  • Int... объявляет вариадический параметр: функция принимает любое число целых аргументов.
  • Внутри numbers становится массивом [Int].
  • reduce(0, +) сворачивает массив в сумму, начиная с нуля.
  • Вызов sum(1, 2, 3, 4) показывает естественный синтаксис без ручного массива.

Внутри тела numbers — это массив ([Int]). Вариадический параметр, как правило, один на функцию и стоит последним среди обычных параметров (до inout).


Параметры inout

По умолчанию аргументы в Swift передаются по значению (для value types — копия). Чтобы функция могла изменить переменную вызывающего, используется inout:

func swapValues(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 1, y = 2
swapValues(&x, &y)

Разбор:

  • inout разрешает функции менять переменные вызывающего кода.
  • Вызов через &x и &y явно показывает передачу по изменяемой ссылке.
  • Временная переменная temp нужна для безопасного обмена значениями.
  • После вызова значения x и y будут поменяны местами.

При вызове обязателен префикс &. Это явный сигнал: "здесь возможен побочный эффект". Нельзя передать let или литерал — только изменяемую переменную.

inout не делает параметр ссылочным типом в смысле class: для структур по-прежнему работает copy-on-write у контейнеров, но сама переменная в области видимости вызывающего обновится.


Trailing closure — замыкание последним аргументом

Если последний параметр — замыкание, его можно вынести за скобки:

func perform(task: String, handler: () -> Void) {
print(task)
handler()
}
perform(task: "backup") {
print("done")
}

Разбор:

  • Функция принимает строковый параметр и замыкание handler, которое не возвращает значение (Void).
  • Вызов использует trailing closure: последнее замыкание вынесено за круглые скобки.
  • Сначала печатается имя задачи task, затем выполняется переданный обработчик.
  • Такой синтаксис широко применяется в SwiftUI и API с callback-логикой.

При нескольких замыканиях trailing применяют к последнему; остальные — в скобках (в Swift 5.3+ — множественные trailing closures для DSL, например SwiftUI).

Связь с управляющими конструкциями и коллекциями:

let ids = users.filter { $0.isActive }.map { $0.id }

Разбор:

  • Цепочка сначала фильтрует пользователей по признаку isActive.
  • map затем преобразует каждый оставшийся объект пользователя в его id.
  • Итогом становится массив идентификаторов активных пользователей.
  • Подход демонстрирует функциональный стиль без промежуточных мутабельных переменных.

Здесь $0 — сокращённый аргумент замыкания; полная форма: { user in user.id }.


Тип функции как значение

Сигнатура (Parameters) -> ReturnType — полноценный тип:

func apply(_ value: Int, using operation: (Int) -> Int) -> Int {
operation(value)
}
let double: (Int) -> Int = { $0 * 2 }
apply(10, using: double)

Разбор:

  • Параметр operation имеет тип функции (Int) -> Int, что делает поведение apply настраиваемым.
  • double — замыкание, которое удваивает входное значение.
  • Вызов apply(10, using: double) передаёт функцию как аргумент и получает 20.
  • Это базовый приём стратегии: алгоритм принимает поведение извне, а не "зашивает" его внутри.

Так задают колбэки, стратегии сортировки, обработчики событий. В современном коде часть колбэков заменена на async/await (асинхронность), но типы функций остаются в стандартной библиотеке (sort, map).


Вложенные и возвращающие функции

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

func makeCounter() -> () -> Int {
var count = 0
func increment() -> Int {
count += 1
return count
}
return increment
}
let counter = makeCounter()
counter() // 1
counter() // 2

Разбор:

  • makeCounter возвращает функцию, которая захватывает переменную count из внешней области.
  • count живёт между вызовами возвращённой функции, поэтому значение накапливается.
  • Каждый counter() увеличивает счётчик и возвращает новое значение.
  • Пример показывает замыкание с состоянием (stateful closure), полезное для фабрик и генераторов.

Возврат функции из функции — основа простых фабрик и замыканий без явного closure литерала.


@escaping и захват

Если замыкание сохраняется и вызывается после возврата функции (асинхронно, в свойстве), параметр помечают @escaping. Иначе компилятор считает, что замыкание завершится до выхода из функции — это безопаснее для памяти.

При захвате self в классах избегайте циклов: [weak self] или [unowned self] в списке захвата. Подробнее — ООП в Swift и справочник.

final class Loader {
private var onFinish: (() -> Void)?

func load(completion: @escaping () -> Void) {
onFinish = completion
DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
self?.onFinish?()
self?.onFinish = nil
}
}
}

Разбор:

  • Параметр completion помечен @escaping, потому что замыкание сохраняется в onFinish и вызывается позже.
  • Без @escaping компилятор выдаст ошибку: замыкание не гарантированно завершится до выхода из load.
  • [weak self] предотвращает retain cycle между Loader и отложенным блоком GCD.
  • self?.onFinish?() безопасно вызывает колбэк только если объект ещё жив.

Соглашения для своего API

СитуацияРекомендация
Публичный метод с 3+ параметрамиОсмысленные внешние метки
"Оператор", математика_ для всех или большинства
Побочный эффект над аргументомinout + ясное имя
Асинхронный колбэк@escaping, явный [weak self] в документации
SwiftUI / DSLTrailing closure, @ViewBuilder (отдельно в фреймворках)

Связанные материалы


Практика проектирования API

Функции в Swift читаются как контракты. Чем понятнее контракт, тем проще поддержка и ревью.


Чеклист для публичной функции

  • Метки параметров отражают смысл аргументов.
  • throws и async стоят там, где реально есть ошибка или ожидание.
  • inout используется только при осознанной мутации данных вызывающего кода.
  • Замыкания, уходящие за пределы функции, помечены @escaping.

Наглядный антипример

func load(_ a: String, _ b: Int, _ c: Bool) {}

func load(resource path: String, retryCount: Int, useCache: Bool) {}

Разбор:

  • Первая функция скрывает смысл аргументов: по вызову непонятно, что именно передаётся.
  • Во второй версии метки resource, retryCount, useCache документируют контракт прямо в сигнатуре.
  • Такой рефакторинг уменьшает ошибки при вызове и ускоряет ревью.
  • Пример иллюстрирует центральную идею статьи: читаемость API важнее минимальной длины кода.

Во втором варианте чтение вызова сразу показывает смысл аргументов.


Полезные продолжения


Сквозной мини-кейс — API сервиса загрузки заказов

Проектируем функцию, которую удобно вызывать и тестировать:

Код ITЗагрузка примера кода…

Разбор:

  • enum SortOrder задаёт явный набор допустимых вариантов сортировки.
  • Параметры функции сочетают обязательный аргумент (userID) и значения по умолчанию (status, order, session).
  • URLComponents и queryItems безопасно собирают URL с учётом optional-фильтра status.
  • try await session.data выполняет сетевой запрос, guard проверяет HTTP-диапазон, затем JSONDecoder декодирует массив Order.
  • switch order задаёт стратегию сортировки и делает поведение функции предсказуемым для вызывающего кода.

Вызов читается как фраза:

let list = try await service.fetchOrders(for: 42, status: .paid, sortedBy: .newest)

Разбор:

  • try await показывает, что вызов одновременно асинхронный и потенциально выбрасывает ошибку.
  • Метки параметров формируют читаемую фразу и явно показывают доменную цель каждого аргумента.
  • Возвращаемое значение присваивается в list, где ожидается массив заказов после фильтрации и сортировки.

Колбэк-версия для legacy-кода оборачивается через withCheckedThrowingContinuation — см. асинхронность.


Анти-паттерны и типичные ошибки

Анти-паттернПримерИсправление
Безметочные параметры в публичном APIconfigure(true, 3, "ru")configure(isEnabled:animated:locale:)
inout для "удобства" без мутации снаружипередача копии, ожидание изменениявернуть новое значение из функции
Забытый @escapingколбэк сохраняется в свойствепометить параметр @escaping
Trailing closure не на последнем местесинтаксическая ошибка или путаницапереставить параметры
10 параметров с defaultнеясно, что обязательноразбить на struct конфигурации
self в escaping без [weak self]утечка в UIViewController[weak self] в capture list