JavaRush /Курсы /Swift SELF /Значения по умолчанию и variadic‑параметры

Значения по умолчанию и variadic‑параметры

Swift SELF
9 уровень , 3 лекция
Открыта

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

Заметьте, как удобно читать: стандартный диапазон 0100 можно не повторять каждый раз (потому что он “типичный”), а если нам нужна настройка — мы явно указываем 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), где непонятно, что чем является.

1
Задача
Swift SELF, 9 уровень, 3 лекция
Недоступна
Рамка сообщения
Рамка сообщения
1
Задача
Swift SELF, 9 уровень, 3 лекция
Недоступна
Ограничитель значений
Ограничитель значений
1
Задача
Swift SELF, 9 уровень, 3 лекция
Недоступна
Режим калькулятора
Режим калькулятора
1
Задача
Swift SELF, 9 уровень, 3 лекция
Недоступна
Консольный логгер
Консольный логгер
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ