1. Значения по умолчанию
Когда вы только начинаете писать функции, часто хочется сделать их “универсальными”: чтобы одна и та же функция умела работать и “просто так”, и “с настройками”. И вот тут новички обычно идут двумя путями: либо создают несколько похожих функций с разными именами, либо заставляют всегда передавать все параметры, даже если 90% времени они одинаковые.
Оба пути быстро ведут к коду‑болоту. В первом случае вы получаете зоопарк вроде printHeader(), printHeaderWithWidth(), printHeaderWithWidthAndChar(). Во втором — вызовы, в которых куча “дежурных” аргументов, и глаз перестаёт видеть важное.
Параметры по умолчанию решают эту проблему очень по‑взрослому: функция остаётся одна, но часть параметров становится необязательной — если вы её не передали, Swift подставит заранее заданное значение.
Синтаксис параметров по умолчанию
Параметр по умолчанию задаётся прямо в сигнатуре функции: name: Type = value. Это выглядит почти как обычное объявление параметра, только с “равно”. И важная мысль: значение по умолчанию задаёт автор функции, а не вызывающий код. То есть “дефолт” — это часть контракта, а не магия из воздуха.
Начнём с максимально простого примера: печать строки с “рамкой” вокруг текста. В обычной жизни вы бы, вероятно, хотели иногда менять символ рамки, но чаще — использовать один и тот же.
import Foundation
func framed(_ text: String, border: String = "*") -> String {
return "\(border) \(text) \(border)"
}
print(framed("Swift")) // * Swift *
print(framed("Swift", border: "#")) // # Swift #
Обратите внимание: в первом вызове мы не передали border, и функция просто использовала "*".
Здесь легко заметить приятный эффект: дефолт отлично дружит с labels. Вызов framed("Swift", border: "#") читается нормально и сразу понятно, что именно меняется.
Дефолты и читаемость
Когда параметров становится больше одного, “значения по умолчанию” начинают работать не как экономия символов, а как способ сохранить мозг разработчика в рабочем состоянии. С дефолтами у вас появляется “стандартное поведение” и “настройка”, и оба варианта выглядят аккуратно.
Представим функцию, которая “зажимает” число в диапазоне (часто это называют clamp): если число меньше минимума — вернуть минимум, если больше максимума — вернуть максимум, иначе вернуть само число.
import Foundation
func clamp(_ value: Int, min minValue: Int = 0, max maxValue: Int = 100) -> Int {
if value < minValue { return minValue }
if value > maxValue { return maxValue }
return value
}
print(clamp(120)) // 100
print(clamp(-10)) // 0
print(clamp(50, min: 10, max: 60)) // 50
Заметьте, как удобно читать: стандартный диапазон 0…100 можно не повторять каждый раз (потому что он “типичный”), а если нам нужна настройка — мы явно указываем min: и max:.
И тут очень важное практическое правило: дефолт хорош тогда, когда он действительно логичен как стандарт. Если вы ставите дефолт “лишь бы параметр был необязательным”, вы ухудшаете контракт функции: её поведение становится менее предсказуемым.
Дефолты в функциях ввода
Когда мы пишем консольные программы, “ввод” почти всегда сопровождается однообразным кодом: вывести подсказку, прочитать readLine(), обработать nil, преобразовать строку в число, подставить запасное значение. Это идеальный кандидат на функцию, а функция — идеальный кандидат на дефолты.
Сделаем небольшой помощник: читать целое число, показывая подсказку. Если пользователь ничего не ввёл или ввёл ерунду — использовать дефолт.
import Foundation
func readInt(prompt: String = "Введите число:", defaultValue: Int = 0) -> Int {
print(prompt, terminator: " ")
let line = readLine() ?? ""
return Int(line) ?? defaultValue
}
let age = readInt(prompt: "Ваш возраст?", defaultValue: 18)
print("Возраст: \(age)") // например: Возраст: 18
Здесь “дефолт” особенно полезен в двух местах:
Первое — prompt: если не передали, программа всё равно ведёт себя адекватно (подсказка будет). Второе — defaultValue: это понятная политика “что делать, если ввод плохой”. Важно, что политика не спрятана где‑то внутри случайного ??, а оформлена как параметр контракта: её можно менять в конкретном вызове.
Если вы сейчас думаете: “а почему не сделать defaultValue просто 0 и не показывать его в параметрах?” — вы как раз начинаете мыслить как автор API: иногда дефолт 0 нормален, а иногда хочется 18, 1, 100, или вообще “повторить ввод”. Повтор ввода — это уже чуть сложнее и требует циклов/валидации, но сама идея “контрактом описать стандартное поведение” — ровно то, что нам нужно.
Дефолты как часть UX
Давайте представим простейшую оболочку CLI: мы читаем строку команды, печатаем подсказку, а если строка пустая — считаем, что пользователь просто нажал Enter и хотел “ничего не делать”.
С параметром по умолчанию это удобно выразить:
import Foundation
func readCommand(prompt: String = "> ") -> String {
print(prompt, terminator: "")
return readLine() ?? ""
}
let cmd = readCommand() // стандартное приглашение
appPrint("Вы ввели:", cmd)
Если позже мы захотим для какой-то части программы поменять приглашение (например, во время “подрежима” или настройки), мы не будем переписывать функцию — мы просто вызовем readCommand(prompt: "setup> ").
И вот это ощущение “я расширяю поведение без копипасты” — одна из главных причин, почему дефолты любят даже суровые разработчики, которые обычно не любят вообще ничего.
“Шапки” и разделители через дефолты
В консольных приложениях часто хочется сделать вывод визуально аккуратнее: заголовки, разделительные линии, пустые строки. Всё это легко превращается в копипасту вида print("-----") в двадцати местах.
Сделаем функцию “заголовок + линия”, где символ линии и длина — настройки, но обычно не нужны:
import Foundation
func printHeader(_ title: String, lineChar: Character = "-", lineLength: Int = 20) {
print(title)
print(String(repeating: lineChar, count: lineLength))
}
printHeader("LibraryCLI")
printHeader("Настройки", lineChar: "=", lineLength: 30)
Тут сразу несколько приятных моментов. Во-первых, сигнатура сама объясняет, что можно менять. Во-вторых, стандартный вызов простой. В-третьих, если вы будете поддерживать единый стиль во всём приложении, вы просто поменяете дефолты в одном месте — и вся программа “переоденется”.
2. Variadic‑параметры
Идея и синтаксис variadic‑параметров
Параметры по умолчанию решают проблему “иногда параметр не нужен”. Variadic‑параметры решают другую проблему: “иногда значений не одно, а много”. Например, print() в Swift может принимать несколько значений сразу — потому что у него variadic‑параметр.
Variadic‑параметр объявляется так: name: Type.... Три точки здесь означают: при вызове можно передать 0, 1, 2, 100 аргументов этого типа.
Напишем sum, который складывает любое количество целых чисел:
import Foundation
func sum(_ numbers: Int...) -> Int {
var total = 0
for n in numbers {
total += n
}
return total
}
print(sum(1, 2, 3)) // 6
print(sum()) // 0
Важный момент: sum() без аргументов — это нормально. Внутри функции numbers будет пустым набором значений (в практической модели — “пустой список”), и цикл просто не выполнится. Такое поведение — часть языка и стандартных правил variadic: variadic можно “не передать”, и это считается эквивалентом пустого набора.
И да: это тот момент, где программист должен выбрать корректный “нулевой результат”. Для суммы это 0. Для произведения, например, логичнее был бы 1. Для “склеивания слов” — пустая строка. И если вы не продумали это заранее, потом получаются странные баги уровня “почему average() делит на ноль”.
Что происходит внутри variadic‑параметра
Снаружи variadic выглядит как “магия”: вы пишете sum(1, 2, 3), а функция принимает “много значений”. Внутри же вам не нужно изобретать велосипед: вы просто проходите по значениям циклом for-in, как мы уже умеем делать со знакомыми нам диапазонами.
Самое полезное здесь для новичка — не пытаться запоминать терминологию уровня “это массив”. Держите простую модель: variadic — это “набор значений”, который можно перебрать. На уровне синтаксиса мы уже умеем всё, что нужно: for n in numbers { ... }.
Схематично это можно представить так:
flowchart TD
A["Вызов: sum(1, 2, 3)"] --> B["numbers внутри функции: 1, 2, 3"]
B --> C["for-in перебирает значения"]
C --> D["total накапливает сумму"]
D --> E["return total"]
Если вы привыкли к мысли “функция получает один аргумент”, variadic — это просто договорённость “получает один параметр, но в нём может быть много значений”.
Variadic и порядок параметров
Теперь важный момент дизайна сигнатуры. Variadic‑параметр выглядит мило, пока он один и стоит в удобном месте. Но если вы начинаете смешивать variadic, параметры по умолчанию и параметры без labels, можно построить сигнатуру, которую потом больно вызывать.
Есть общее практическое правило: variadic почти всегда логично ставить “вокруг” основной сущности, а все дополнительные настройки делать отдельными параметрами с labels.
Хороший пример — склеивание слов, где список слов variadic, а разделитель — параметр по умолчанию:
import Foundation
func joinWords(_ words: String..., separator: String = " ") -> String {
var result = ""
var isFirst = true
for w in words {
if isFirst {
result = w
isFirst = false
} else {
result += separator + w
}
}
return result
}
print(joinWords("Swift", "is", "fun")) // Swift is fun
print(joinWords("Swift", "is", "fun", separator: "-")) // Swift-is-fun
Здесь читаемость поддерживается двумя вещами: variadic без label в начале (это “главные” слова), и separator: как явная настройка.
Интересный факт (скорее как заметка для кругозора): современный Swift разрешает несколько variadic‑параметров, но тогда параметры, идущие после variadic, должны иметь внешние labels, иначе вызов становится неоднозначным. Мы в этом курсе почти всегда будем держаться одного variadic‑параметра, потому что так проще читать и писать код.
Комбинируем дефолты и variadic в мини‑CLI
Теперь давайте сделаем шаг к нашему учебному консольному приложению. Мы хотим, чтобы программа аккуратно печатала сообщения: иногда одно слово, иногда несколько, иногда с разным разделителем. Да, звучит как “давайте изобретём print”, но на самом деле мы изобретаем не print, а единый стиль вывода для приложения.
Сделаем маленький “логгер для бедных”. Он будет печатать слова через разделитель, а ещё позволит задавать префикс строки (например, ">" или "•").
import Foundation
func appPrint(_ parts: String..., prefix: String = "", separator: String = " ") {
let text = joinWords(parts..., separator: separator)
print(prefix + text)
}
appPrint("help") // help
appPrint("add", "book", "Swift") // add book Swift
appPrint("Ошибка:", "неизвестная команда", prefix: "⚠️ ") // ⚠️ Ошибка: неизвестная команда
Обратите внимание на фокус: наша функция не делает ничего “умного”. Она просто стандартизирует вывод. А стандартизация — это то, что в больших приложениях экономит время и нервы.
Технически тут есть важная деталь: чтобы “перекинуть” variadic дальше в другую функцию, мы используем parts... при вызове joinWords. Это выглядит странно в первый раз, но читается как “передай все значения дальше”. Если это пока кажется магией — нормально, просто воспринимайте как специальный синтаксис для вариадиков.
3. Типичные ошибки
Ошибка №1: ожидание, что “дефолт возьмётся из контекста”, а не из объявления функции.
Иногда кажется, что если у вас есть переменная defaultSeparator, то функция “как-нибудь догадается”. Нет: значение по умолчанию — это часть объявления func. Если хотите менять дефолт динамически, придётся передавать аргумент явно или хранить настройку вне функции и использовать её внутри (но это уже другой разговор и другая ответственность).
Ошибка №2: превращение функции в “комбайн” из десяти дефолтных параметров.
Да, технически можно сделать func render(text:..., color:..., font:..., align:..., width:..., height:...). Но читаемость рухнет: вы уже не понимаете, что функция делает “по умолчанию”. Хороший дефолт — это стандартное поведение, а не способ спрятать сложность под ковёр.
Ошибка №3: забыли продумать крайний случай variadic = 0 аргументов.
Variadic‑параметр можно не передать, и тогда внутри функции вы получите пустой набор значений. Если ваша логика предполагает хотя бы одно значение (например, вы делите сумму на количество), вам нужна проверка “пусто / не пусто” и понятная политика, что делать в пустом случае.
Ошибка №4: попытка передать variadic как “одним пакетом”, а не списком.
Новички иногда пытаются сделать что-то вроде sum([1,2,3]). В variadic так не работает: вы передаёте значения через запятую: sum(1, 2, 3). Мы ещё дойдём до массивов и тогда обсудим, как соединяются мир variadic и мир коллекций — но сейчас просто держим в голове правило вызова.
Ошибка №5: запутались в labels при сочетании variadic и обычных параметров.
Смешивание _, variadic и параметров по умолчанию может сделать вызов нечитаемым или даже невозможным. В современном Swift есть правила, которые заставляют параметры после variadic иметь labels, чтобы вызов не был неоднозначным. Если вы видите, что компилятор “просит label” — это не вредность, а попытка спасти вас от вызовов вида f(1, 2, 3, 4, 5), где непонятно, что чем является.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ