1. Зачем нужны члены типа
Когда вы только начинаете писать классы, легко привыкнуть к мысли: «Раз это класс — значит, я создаю объект и вызываю методы на объекте». Но в реальных программах часто нужно иметь функциональность ещё до создания экземпляра: константы по умолчанию, фабричные методы, счётчики созданных объектов, короткие утилиты форматирования. И хочется, чтобы это было связано с типом логически, а не болталось отдельной глобальной функцией.
У Swift для этого есть разделение на два уровня API: instance members (то, что принадлежит конкретному объекту) и type members (то, что принадлежит типу в целом). Вызов вида user.rename() — это экземплярный метод, он требует объект. А вызов вида User.makeAnonymous() — это член типа: он вызывается через имя типа и не требует заранее созданного объекта.
Важно: члены типа — это не «какая-то магия из будущих тем». Это просто аккуратный способ выразить мысль: «Это относится ко всему классу, а не к одному экземпляру». И как раз здесь появляется интересная развилка: некоторые вещи мы хотим сделать непереопределяемыми, а некоторые — наоборот, хотим оставить возможность «подстроить поведение» в производном типе (идея переопределяемости).
2. static members: фиксированное поведение
Если вы пишете static у свойства или метода внутри класса, вы говорите компилятору примерно так: «Это принадлежит типу, и я не предполагаю, что кто-то будет менять это поведение через наследование». Можно мысленно переводить static как «жёстко прибито гвоздями к этому типу».
На практике static чаще всего используют для двух вещей. Во-первых, это константы и «дефолтные» значения: например, «сколько книг показываем на странице», «какой разделитель используем в выводе», «какой формат даты считаем стандартным». Во-вторых, это фабричные методы — удобные способы создать экземпляр с преднастройками.
Пока звучит почти так же, как class func / class var. Но отличие (то самое, ради которого мы здесь собрались): static в классе не предназначен для переопределения. Даже если позже появится подкласс, он не сможет «заменить реализацию» static-метода.
Это полезно именно как ограничение: иногда вы хотите быть уверены, что логика «по умолчанию» всегда одна и та же, без сюрпризов. Программирование вообще состоит из двух частей: написать код и потом не плакать, когда он живёт своей жизнью. static помогает со второй частью.
import Foundation
final class LibraryConfig {
static let maxFeaturedBooks: Int = 3
static func bannerTitle() -> String {
"Добро пожаловать в нашу мини-библиотеку!"
}
}
print(LibraryConfig.maxFeaturedBooks) // 3
print(LibraryConfig.bannerTitle()) // Добро пожаловать в нашу мини-библиотеку!
Здесь LibraryConfig вообще можно не создавать как объект. Это «коробка» для настроек на уровне типа.
3. class members: допускают переопределение
Ключевое слово class в Swift многозначное (как слово “bank” в английском): оно может означать «объявление класса» (class User { ... }), а может означать «член типа, который допускает переопределение» (class func ... / class var ...). И именно второй смысл нам нужен в этой лекции.
Когда вы объявляете type member с ключевым словом class, вы как бы оставляете «дверь приоткрытой»: если кто-то создаст подкласс, то он сможет определить свою версию этого метода/свойства. Это похоже на то, как вы в быту говорите: «Я готовлю борщ по этому рецепту, но если кто-то в семье захочет добавить фасоль — пусть добавляет». С static фасоль добавлять нельзя: рецепт запаян.
Важно: мы не уходим глубоко в механику наследования и override (это отдельная большая тема). Но сам смысл различия важно почувствовать сейчас: class type member — это «на уровне типа, но с возможностью быть другим у потомков».
Ещё один практический нюанс: class var в Swift — это обычно computed property, то есть свойство с телом { ... }. Хранить stored property как class var нельзя: хранить умеет static, а class чаще используют именно для вычисляемых значений или функций.
import Foundation
class ReportFormatter {
class func header() -> String { "=== ОТЧЁТ ===" }
}
class DebugReportFormatter: ReportFormatter {
override class func header() -> String { "=== DEBUG ОТЧЁТ ===" }
}
print(ReportFormatter.header()) // === ОТЧЁТ ===
print(DebugReportFormatter.header()) // === DEBUG ОТЧЁТ ===
Здесь важно не столько запомнить синтаксис override (он ещё будет закрепляться), сколько увидеть идею: class func оставляет возможность другой реализации в «родственных типах».
Сравнение static и class
Когда ключевые слова похожи, мозг начинает экономить энергию и говорит: «Да какая разница, оба же “на уровне типа”». Разница есть, и она как раз про переопределяемость. Чтобы было проще держать в голове, сведём всё в таблицу.
После небольшого знакомства становится видно, что static — это «фиксированное», а class — это «гибкое». И это не про «хорошо/плохо», а про то, какой контракт вы хотите задать.
| Что сравниваем | static member в классе | class member в классе |
|---|---|---|
| Вызывается как | |
|
| Можно переопределять в подклассе | нет (идея: «прибито к типу») | да (идея: «можно подстроить») |
| Подходит для stored properties | да (static let/var) | нет (обычно только computed class var) |
| Подходит для фабрик и утилит | да | да |
| Типовой смысл | «единое правило для всех» | «общая идея, но допускаем варианты» |
Тут полезно запомнить одно «почти бытовое» правило. Если значение должно быть одинаковым для всех и вы не хотите, чтобы его меняли через наследование, берите static. Если вы заранее понимаете, что «в разных вариантах класса» логика может отличаться, и это нормальная часть дизайна, берите class.
И ещё маленькая деталь, которая помогает не путаться: static встречается и в struct, и в enum, и в class. А class как модификатор члена типа встречается только внутри классов, потому что именно у классов есть наследование как идея.
4. Практика: отчёт мини‑библиотеки
Чтобы знания не улетели из головы сразу после Run, давайте встроим static и class members в маленький кусочек нашего учебного консольного приложения «мини‑библиотека». Мы не строим полноценную систему, нам важно почувствовать, где это удобно.
Представим, что у нас есть сущность книги (пока максимально простая) и мы хотим печатать «отчёт»: список книг с заголовком. Заголовок в обычном режиме один, в «отладочном» — другой. Это как раз идеальное место, где class func выглядит естественно.
import Foundation
struct Book {
let id: Int
let title: String
}
class LibraryReport {
class func header() -> String { "Список книг" }
static func printBooks(_ books: [Book]) {
print(header())
for b in books { print("#\\(b.id): \\(b.title)") }
}
}
Обратите внимание на связку: метод печати отчёта мы сделали static, а заголовок — class. Почему так? Потому что сама процедура печати «печатай заголовок и строки» у нас одна и та же (мы не хотим, чтобы её «перепридумали»), а вот заголовок — штука, которую логично менять в «вариациях» отчёта.
Теперь добавим «отладочную» версию отчёта, которая меняет только заголовок. Идея: печать остаётся той же, но заголовок другой.
import Foundation
class DebugLibraryReport: LibraryReport {
override class func header() -> String { "Список книг (DEBUG)" }
}
let books = [Book(id: 1, title: "Dune"), Book(id: 2, title: "Solaris")]
LibraryReport.printBooks(books)
// Список книг
// #1: Dune
// #2: Solaris
DebugLibraryReport.printBooks(books)
// Список книг (DEBUG)
// #1: Dune
// #2: Solaris
Если вам сейчас кажется, что это «слишком много ради заголовка» — вы правы, и это нормально. Мы учимся на маленьком примере, чтобы потом не строить огромную систему из глобальных констант и случайных функций.
5. TypeName.member vs Self.member
Когда вы внутри кода класса обращаетесь к члену типа, вы можете написать либо LibraryReport.header(), либо Self.header(). На старте кажется, что это просто два способа написать одно и то же. В простых случаях — да. Но как только появляется идея «может быть переопределено», Self начинает звучать совсем иначе.
Интуитивная мысль такая: TypeName.something — это прямое указание на конкретный тип. А Self.something — это «то же самое, но относительно текущего типа». И именно это «относительно текущего типа» позволяет логике корректно работать, если class member переопределён.
Иногда это объясняют так: в контексте класса Self означает «динамический тип self». Поэтому обращение к class members через «динамический тип» и через имя типа может вести себя по-разному, если члены не final.
Давайте перепишем печать отчёта так, чтобы она вызывала Self.header(). Тогда «печаталка» остаётся одна, но она корректно подхватывает заголовок от производного типа.
import Foundation
class LibraryReport2 {
class func header() -> String { "Список книг" }
static func printHeader() {
print(Self.header())
}
}
class DebugLibraryReport2: LibraryReport2 {
override class func header() -> String { "Список книг (DEBUG)" }
}
LibraryReport2.printHeader() // Список книг
DebugLibraryReport2.printHeader() // Список книг (DEBUG)
Здесь ключевой момент не в том, что «надо всегда писать Self». А в том, что если вы сознательно выбрали class (то есть допускаете переопределение), то и вызывать такой member часто логичнее через Self, чтобы уважать эту идею.
Как выбирать между static и class
Когда вы начинаете проектировать API класса, вы фактически пишете мини‑договор с будущим собой (и, возможно, с другими разработчиками). static и class — это не просто синтаксис, а способ выразить намерение: «это фиксировано» или «это допускает вариации». И очень полезно выбирать не «как привычнее», а «какой контракт я хочу».
Если вы делаете static let defaultValue, вы говорите: «Это значение — часть определения типа. Оно не должно внезапно становиться другим в потомках». Это удобно для констант, форматных строк, маленьких утилит, фабрик с жёстким поведением.
Если вы делаете class func makeDefault() или class var header: String { ... }, вы оставляете пространство для расширения поведения через наследование. Это бывает полезно, когда класс явно задуман как «база» для нескольких вариантов поведения. Даже если вы прямо сейчас не пишете подклассы, вы можете заранее заложить такую точку расширения.
При этом есть ловушка: новичку легко начать делать всё class, потому что «ну вдруг пригодится». Чем больше «возможностей расширения», тем больше неожиданных комбинаций поведения вы допускаете. Поэтому по умолчанию спокойнее начинать со static, а class вводить там, где вы действительно понимаете сценарий вариативности.
6. Типичные ошибки при работе с static и class members
Ошибка №1: путать static и class как «два способа написать одно и то же».
Поначалу это выглядит так: и там, и там вызов через TypeName.member. Но смысл разный: static — «фиксированное поведение», class — «может быть переопределено». Если вы не различаете эти намерения, вы либо случайно запрещаете нужную вариативность, либо случайно разрешаете её там, где она вам не нужна.
Ошибка №2: пытаться переопределить static member и удивляться, почему компилятор против.
Это классическая ситуация: вы сделали static func header(), потом решили, что в «особой версии» нужно поменять заголовок, и написали override. Компилятор справедливо отвечает: «Нельзя». И это не вредность Swift, а следствие вашего же контракта: static как раз про то, что переопределений не предполагается.
Пример ошибки компиляции:
class A {
static func f() { }
}
class B: A {
override static func f() { } // ❌ так нельзя
}
Ошибка №3: использовать TypeName.member там, где вы хотели поддержать вариативность, и тем самым «прибить» поведение.
Если вы задумали class func header(), но внутри логики печати написали LibraryReport.header(), вы фактически сказали: «мне всё равно на переопределение». Потом вы создадите подкласс, а заголовок не поменяется — и это будет выглядеть как мистическая ошибка, хотя проблема просто в том, что вызов «прибит» к базовому типу. В таких местах обычно логичнее Self.header().
Ошибка №4: превращать static var в глобальное изменяемое состояние без причины.
static var в классе — это одно значение на весь тип. Это мощно, но опасно: если много мест в программе будут менять этот static var, поведение станет трудно предсказуемым (в стиле «почему оно сегодня печатает 3 книги, а вчера печатало 5?»). На этом этапе курса достаточно запомнить простое правило: начинайте с static let, а к static var переходите только если реально нужно изменяемое состояние на уровне типа.
Ошибка №5: путать class в объявлении типа и class в объявлении члена типа.
class User { ... } — это «объявили класс». class func make() — это «объявили член типа, который допускает переопределение». Это разные смыслы одного и того же слова. Если в голове они смешиваются, код начинает казаться страшнее, чем он есть. Обычно помогает привычка вслух читать class func как «переопределяемый метод типа», а static func как «фиксированный метод типа».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ