1. Имена функций в Swift: base name и labels
Если вы только начинаете, кажется, что главное — чтобы программа работала: добавили книгу, удалили книгу, вывели список — ура. Но в проекте чуть побольше выясняется, что вы гораздо чаще читаете код, чем пишете. А API — это именно то, что вы читаете на call site: в месте вызова.
Представьте, что ваш модуль Domain — это кофемашина. Можно сделать 17 кнопок без подписей и инструкцию на 40 страниц. А можно сделать три большие кнопки: “Espresso”, “Americano”, “Milk”. В обоих случаях кофе получится. Но во втором случае шанс, что пользователь случайно сварит “латте без чашки”, заметно ниже. В коде роль подписей выполняют имена функций и labels.
В языках вроде Java/C# многие привыкли: «имя метода — это слово до скобок». В Swift всё хитрее: argument labels являются частью имени. Исторически это поддерживает две важные вещи: читаемость вызова и возможность иметь несколько перегрузок с одинаковым base name, но разными labels (например, как у UIKit‑подобных API). Эта идея явно проговаривается в материалах Swift Evolution про compound names и различение перегрузок по меткам аргументов.
Labels — часть имени функции
То есть, грубо говоря, функция:
func move(from: Int, to: Int)
имеет имя (в терминах Swift) move(from:to:), а не просто move.
Это неожиданно полезно в реальной жизни. Например, вы можете иметь несколько методов insertSubview, которые различаются по смыслу именно через labels — «вставить по индексу», «вставить над», «вставить под». Такой пример как раз приводится в обсуждении именования функций с labels.
Call site как предложение
Когда говорят «читается как предложение», обычно имеют в виду: если вы смотрите на строку кода, вам не надо лезть в реализацию, чтобы понять смысл параметров. Swift прямо поощряет этот стиль: многие решения языка и стандартной библиотеки исторически приводили к тому, что labels стали “первым классом” читаемости.
Показательный момент: в современном Swift нет «особого случая» для первого параметра, и метка первого аргумента ведёт себя так же, как и остальные — это результат унификации поведения labels.
Сравним две версии одного и того же действия в нашем CLI‑проекте LibraryCLI.
Пример: плохой call site без labels
struct Library {
mutating func addBook(_ book: Book) {
// ...
}
}
Вызов будет таким:
library.addBook(book)
Вроде нормально. Но проблема появляется, когда параметров становится два-три, или когда типы одинаковые (например, String, String). Тогда без labels легко перепутать.
Пример: лучше — смысл читается из вызова
struct Library {
mutating func add(_ book: Book) {
// ...
}
mutating func removeBook(withID id: BookID) {
// ...
}
}
Вызовы:
library.add(book)
library.removeBook(withID: id)
Здесь “withID” — это маленькая подсказка мозгу: «сейчас будет ID, а не название, не индекс и не “что-то ещё”».
Когда скрывать label через _
Очень хочется сделать API «короче» и начать везде писать _, убирая внешние метки. Но в Swift _ — это не «делаем красиво», а «мы уверены, что смысл аргумента очевиден без подписи».
Хорошее правило для новичка: если вы сомневаетесь, нужен ли label — скорее всего, нужен. Swift в целом двигается в сторону того, чтобы labels помогали читателю, а не мешали ему (это видно и по подходу к переводу Objective‑C API в Swift‑стиле, где активно добавляются/нормализуются labels и выкидываются лишние слова).
Пример: _ уместен, когда аргумент «главный»
struct BookID {
let rawValue: String
}
func findBook(_ id: BookID) {
print("Searching \(id.rawValue)")
// Searching 123
}
Здесь _ можно оправдать: в контексте findBook(...) аргумент почти всегда «и так понятно что это ID».
Пример: labels спасают, когда параметры легко перепутать
func move(from startIndex: Int, to endIndex: Int) {
print("move from \(startIndex) to \(endIndex)")
// move from 2 to 10
}
move(from: 2, to: 10)
Если убрать labels, вы получите классическое «а что было первым: откуда или куда?». А такие ошибки особенно неприятны тем, что код компилируется и “даже работает”… но делает не то.
Предлоги в labels: from, to, in, at, with, for
Swift любит, когда labels выглядят как предлоги и образуют фразу. Более того, язык даже специально решал проблему, что некоторые хорошие предлоги совпадают с ключевыми словами (например, in). В Swift Evolution отдельно обсуждалось, что такие keywords стоит разрешить как argument labels, потому что они действительно улучшают выразительность API.
Это означает, что “человеческий” API может выглядеть так:
indexOf(value, in: collection)
И это не «странность», а буквально то, чего Swift добивается — чтобы интерфейс читался как предложение.
Для нашего LibraryCLI это переводится в практику:
- searchBooks(containing:) — “ищи книги, содержащие строку”
- removeBook(withID:) — “удали книгу с таким ID”
- updateBook(withID:to:) — “обнови книгу с ID на новое значение”
Смысл labels здесь — показать роль аргумента, а не его тип.
3. Нейминг по эффекту и по смыслу
Когда вы проектируете API, вы не только называете функцию — вы даёте обещание: “эта операция делает вот это”. Если назвать метод process() или handle(), вы обещаете… ничего. Компилятор не против, но пользователи вашего кода будут страдать молча (а потом громко).
Глаголы для действий и «не обещай лишнего»
Очень практичный приём: различать операции по эффекту.
Если метод меняет состояние, имя обычно глагольное: add, remove, update. Если метод возвращает вычисленное значение, имя часто выглядит как существительное/прилагательное: count, isEmpty, title.
В стандартной библиотеке и вокруг неё много примеров, где нейминг подталкивает к правильному чтению и правильному ожиданию. Исторически Swift также старался «вырезать лишние слова, повторяющие тип», чтобы оставался смысл, а не тавтология.
Пример: мутирующая операция в Library
struct Book {
let id: String
let title: String
}
struct Library {
private(set) var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
}
Обратите внимание: add — коротко и глагольно. addBook тоже можно, но если контекст уже “Library”, слово Book иногда становится лишним (мы ещё вернёмся к “omit needless words”).
Булевы свойства и методы: is…, has…, can…
Булевы значения — особая боль новичка: true/false сами по себе ничего не объясняют, всё решает имя.
Swift‑стиль обычно такой: булевы свойства читаются как утверждения о получателе: isEmpty, isValid, hasPrefix, canUndo. Даже при переводе Objective‑C API в Swift отдельно подчёркивалось, что булевым свойствам логично добавлять is, чтобы они читались как утверждение.
Пример: плохой bool, который не читается
struct LibraryCLIConfig {
let verbose: Bool
}
func verbose(_ config: LibraryCLIConfig) -> Bool {
config.verbose
}
Вызов if verbose(config) { ... } выглядит странно: “если verbose(config)… что?”
Пример: лучше — читается как фраза
struct LibraryCLIConfig {
let isVerbose: Bool
}
func isVerbose(_ config: LibraryCLIConfig) -> Bool {
config.isVerbose
}
Теперь if isVerbose(config) { ... } читается почти как английское предложение: “if is verbose”.
Omit needless words: убираем слова, которые повторяют контекст
Одна из самых частых ошибок — «назвать всё максимально подробно»: addBookToLibrary(book: Book). Кажется, что так понятнее. Но на практике это превращается в шум.
Swift‑подход часто такой: если контекст уже понятен из типа/места, слово можно убрать. В документах про перевод Objective‑C имён в Swift показано, как из API убираются куски, которые повторяют тип, и как результат становится ближе к “читаемости по смыслу”, а не по тавтологии.
Сравним на примерах, близких к нашему LibraryCLI.
| Плохо (шумно) | Лучше (смысл) | Почему |
|---|---|---|
|
или |
“Library” уже в типе получателя |
|
|
“ByID” — лишнее, withID уже говорит роль |
|
|
“FromLibrary” лишнее, мы и так в Library |
Важно: “omit needless words” не означает “делаем всё коротким”. Оно означает “убираем то, что не добавляет смысла”.
4. Перегрузки и ссылки на методы: роль labels
Swift позволяет иметь несколько методов с одинаковым base name, которые различаются labels. Это удобно, но требует дисциплины: labels должны действительно отражать разные смыслы, а не быть «синонимами ради синонимов».
Перегрузки с одним base name
В обсуждении generalized naming приводится классическая ситуация: один base name (insertSubview), три разных действия, и именно labels делают API однозначным.
Перенесём идею в Library.
Пример: несколько способов поиска, различаем по labels
struct Library {
private(set) var books: [Book] = []
func book(withID id: String) -> Book? {
books.first { $0.id == id }
}
func books(containing titlePart: String) -> [Book] {
books.filter { $0.title.contains(titlePart) }
}
}
Вызовы будут выглядеть так:
let one = library.book(withID: "42")
let many = library.books(containing: "Swift")
Одно и то же слово book/books, но разные “формы”: один объект vs много объектов, и это видно прямо из сигнатуры.
Когда метод передаётся как значение
Эта часть кажется «теорией», пока вы не начнёте активно использовать замыкания и функции как значения. В Swift бывает так, что несколько методов имеют один base name, и без указания labels ссылка на метод становится неоднозначной.
В Swift Evolution по generalized naming это описано прямо: если есть три insertSubview, то someView.insertSubview — неоднозначно, и Swift ввёл/обсуждал синтаксис, позволяющий ссылаться на метод вместе с labels, вроде insertSubview(_:at:).
Для нашего курса это полезно как мысль: labels — это не “косметика”, а часть имени, которая иногда реально влияет на то, сможете ли вы удобно использовать API в более функциональном стиле.
5. Мини‑рефакторинг LibraryCLI: делаем API «разговорным»
Сейчас соберём несколько маленьких кусочков в одну линию. Мы не строим новый модуль и не уходим в архитектуру; наша цель — показать, как нейминг меняет ощущение от кода, хотя логика почти та же.
Шаг 1: доменные типы с понятными именами
struct BookID: Hashable {
let rawValue: String
}
struct Book {
let id: BookID
let title: String
}
BookID говорит сам за себя. rawValue — нормальная конвенция для “обёрток” вокруг примитивов.
Шаг 2: Library с API, который читается в вызове
struct Library {
private(set) var books: [Book] = []
mutating func add(_ book: Book) {
books.append(book)
}
func book(withID id: BookID) -> Book? {
books.first { $0.id == id }
}
}
Вызовы выглядят естественно:
library.add(book)
let found = library.book(withID: id)
Обратите внимание: мы используем withID, а не byID или id:. Это не “единственно правильное” слово, но оно хорошо работает как часть фразы.
Шаг 3: команды CLI тоже выигрывают от нейминга
enum Command {
case add(title: String)
case find(id: String)
}
Да, это не labels в вызове функции, но принцип тот же: add(title:) читается лучше, чем add(String) — особенно когда у команды несколько параметров.
6. Типичные ошибки в API‑дизайне Swift
Ошибка №1: “Сделаю покороче и уберу все labels через _”.
Поначалу кажется, что без labels код компактнее. Но как только появляется два параметра одного типа, вы начинаете путать аргументы местами. Самое неприятное, что компилятор часто не может вас спасти: String и String выглядят одинаково. Хороший стиль Swift как раз ценит labels, и исторически язык двигался к унификации поведения меток для всех параметров, включая первый.
Ошибка №2: “Назову метод максимально подробно: addBookToLibrary”.
Такое имя создаёт иллюзию понятности, но на деле превращает вызов в тавтологию: library.addBookToLibrary(book). Swift‑подход часто обратный: убирать слова, которые повторяют информацию, уже известную из контекста типа. Такой стиль особенно виден на примерах «очистки» имён при переводе Objective‑C API в Swift‑манеру.
Ошибка №3: булевы значения названы так, что true/false непонятны.
Если у вас flag, mode, enabled, ok — это красный флаг (простите за каламбур). Вызов if config.verbose { ... } ещё терпим, но когда булево значение спрятано за функцией verbose(config) — читателю приходится расшифровывать, что именно значит true. В Swift часто используют is/has/can, и даже при автоматическом переводе API в Swift отмечалась полезность префикса is для булевых свойств.
Ошибка №4: labels подобраны как «синонимы», а не как разные смыслы.
Если вы делаете removeBook(by:), removeBook(with:), removeBook(using:) — это не выразительность, это путаница. Перегрузки по labels в Swift действительно существуют и полезны, но они должны отражать разные смысловые операции, как в классическом примере с одним base name и несколькими вариантами поведения, различаемыми по labels.
Ошибка №5: “универсальная функция” с названием handle/process/doStuff.
Это самая частая причина, почему проект через месяц становится нечитаемым: вы открываете файл и видите service.handle(...), manager.process(...), controller.do(...). Такие имена не закрепляют контракт. Гораздо полезнее потратить ещё 30 секунд и назвать действие по смыслу: add, remove, search, parse, format. Да, это скучно. Но скучный код — часто самый надёжный (и это тот вид скуки, за который платят зарплату).
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ