JavaRush /Курсы /Swift SELF /Computed properties и subscript в extension

Computed properties и subscript в extension

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

1. Зачем computed properties и subscript

Когда проект растёт, вы замечаете странную вещь: логика вроде простая, но по коду размазана. То форматирование строки повторяется в трёх местах, то формула вычисления индекса появляется то тут, то там. В такие моменты хочется иметь «умные поля» и «умный доступ по скобкам», чтобы код читался как нормальная мысль, а не как протокол допроса.

Computed properties и subscripts в Swift — это как раз такие «умные члены типа». Формально это свойства и индексаторы, которые запускают ваш код при чтении/записи. Номинальные типы Swift (struct/class/enum) действительно поддерживают computed properties и subscripts как полноценные members, у которых можно определить get и set.

Главная идея лекции: мы добавляем поведение поверх уже имеющегося состояния. То есть храним данные там же, где хранили, а поверх добавляем удобные «ручки» для чтения/изменения.

2. База примеров: Book и Library

Чтобы примеры не были «про сферических котиков в вакууме», давайте продолжим развивать наше учебное консольное приложение-менеджер библиотеки. Пусть у нас есть книга и библиотека как коллекция книг. Это не финальная архитектура и не «идеальный домен», а удобный песочничный фундамент для сегодняшней темы.

Пример (база для дальнейших фрагментов — добавьте в один файл и дальше дописывайте extensions):

import Foundation

struct Book {
    let id: Int
    var title: String
    var author: String
    var year: Int
}

struct Library {
    var books: [Book] = []
}

Эти два типа — наше «хранилище данных». Сегодня мы будем расширять их так, чтобы код, который пользуется библиотекой, стал короче и яснее, а сами данные остались там же.

3. Computed property в extension

Computed property — это свойство, которое не хранит значение, а вычисляет его в get. И это важная мысль: computed property — не «ещё одна переменная», это скорее «короткая функция без скобок». Поэтому computed properties отлично дружат с extension: мы добавляем поведение, не меняя память типа.

На практике computed property полезна, когда вы хотите получить «производное» значение: красивую строку для вывода, краткое описание, отформатированный заголовок, возраст книги и т.п.

Пример (добавьте к коду из раздела 2):


extension Book {
    var displayLine: String {
        "\(id). \(title) — \(author) (\(year))"
    }
}

// Пример использования:
let b = Book(id: 1, title: "Swift", author: "Apple", year: 2014)
print(b.displayLine) // 1. Swift — Apple (2014)

Обратите внимание: мы не добавили ни одного нового stored property. Мы просто дали типу «умение красиво представляться». И это сразу повышает качество любого кода, который печатает книгу: вместо форматирования строк в каждом месте — один computed property.

Маленькая схема: что происходит при чтении computed property

flowchart LR
    A["Код читает book.displayLine"] --> B["Вызывается getter (get)"]
    B --> C["Строится строка из stored properties"]
    C --> D["Возвращаем результат"]

С точки зрения мозга это звучит как «дай мне displayLine». С точки зрения компилятора — это вызов вашего кода.

4. Computed property с get/set

Иногда хочется не только читать производное значение, но и удобно обновлять исходные данные через «красивый вход». Это момент, где появляется set. В set вам доступно специальное имя newValue — то значение, которое присваивают.

Важно понимать, что setter computed property всё равно работает через имеющееся состояние. Он не создаёт память из воздуха. Он лишь изменяет stored properties или вызывает методы, чтобы привести внутреннее состояние к нужному виду.

Кстати, в документах по языку часто подчёркивается, что newValue — это значение, которое приходит в setter computed property или subscript.

Пример: нормализуем заголовок книги через «умное свойство»

Представим типичную CLI-боль: пользователь вводит " swift basics " — пробелы как будто в кредит. Мы хотим хранить title аккуратно, но не хотим каждый раз руками делать trim в коде команд.

Пример (добавьте к предыдущему коду):

extension Book {
    var normalizedTitle: String {
        get { title.trimmingCharacters(in: .whitespacesAndNewlines) }
        set { title = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
    }
}

var book = Book(id: 2, title: "   Swift   ", author: "Me", year: 2023)
book.normalizedTitle = "  Swift 6.2  "
print(book.title) // Swift 6.2

Здесь normalizedTitle — это «вход/выход с правилами». Чтение отдаёт аккуратный вариант, запись тоже приводит данные к аккуратному виду.

Важная ловушка: случайная рекурсия

Когда вы пишете computed property, нельзя внутри неё обращаться к ней же, иначе получится бесконечный вызов getter’а или setter’а. То есть так нельзя:

// ПЛОХО: get вызывает сам себя
var x: String { get { x } }

Правильное правило звучит почти как совет психолога: «говорите с базовой сущностью, не с отражением». Внутри normalizedTitle мы обращаемся к title, а не к normalizedTitle.

Когда computed property становится плохой идеей

Computed property выглядит как «просто поле». Поэтому люди (и вы через месяц тоже) начинают ожидать, что чтение свойства дешёвое и безопасное. Но computed property может делать работу: считать, фильтровать, искать, ходить в календарь, форматировать, а иногда и выполнять дорогие операции. В обсуждениях дизайна Swift отдельно отмечается, что разработчики часто предполагают “stored property” модель и не ждут серьёзных вычислений при доступе к свойству.

Практическое правило простое: если вычисление тяжёлое, лучше сделать метод, чтобы у читателя кода сработала «сигнализация». book.buildSomething() выглядит как действие, а book.something выглядит как «просто достали значение».

В нашем учебном CLI displayLine и normalizedTitle — хорошие кандидаты: это дёшево, локально и предсказуемо.

5. subscript в extension

Если computed property — это «умное поле», то subscript — это «умный доступ через квадратные скобки». В массивах и словарях вы давно им пользуетесь: arr[i], dict[key]. Важно знать, что в Swift это тоже реализовано как subscript. Более того, даже у Array индексатор — это computed subscript, а не какая-то магия на уровне железа.

Мы можем добавить свой subscript и к своим типам, чтобы спрятать неинтересную механику доступа и сделать код читаемее.

Пример: доступ к книге по id через library[id: 10]

Мы хотим писать так:

let book = library[id: 10]

а не каждый раз вручную искать в массиве через цикл.

Пример (добавьте к коду из раздела 2):

extension Library {
    subscript(id id: Int) -> Book? {
        books.first { $0.id == id }
    }
}

var lib = Library(books: [
    Book(id: 1, title: "Swift", author: "Apple", year: 2014),
    Book(id: 2, title: "Algorithms", author: "Someone", year: 2020)
])

print(lib[id: 2]?.title as Any) // Optional("Algorithms")

Обратите внимание на контракт: subscript возвращает Book?. Это честно. Если книги нет — вернём nil. И теперь Optional chaining выглядит естественно.

Кстати, идея «subscript возвращает optional и дальше можно делать цепочку ...?[...]?... » встречается в реальных паттернах работы с древовидными данными. Например, в примерах про JSON можно встретить subscript, возвращающий Optional, чтобы можно было писать цепочки вроде json[0]?["name"]?....

6. subscript с get/set

Read-only subscript — уже полезно. Но иногда хочется уметь обновлять через тот же синтаксис: library[id: 2] = .... Это делает API очень «домашним»: похоже на словарь, только с вашей логикой внутри.

Тут особенно важно заранее договориться о контракте: что значит «присвоить nil»? Удалить? Проигнорировать? Упасть? Swift не запрещает вам выбрать любой вариант, но хороший API должен быть предсказуемым.

Сделаем простой контракт: если присваиваем книгу — делаем upsert (обновить, если есть, иначе добавить). Если присваиваем nil — удаляем книгу с таким id.

Пример (добавьте к Library из раздела 2; этот subscript заменяет read-only версию из раздела 5 — оставьте только один вариант):

extension Library {
    subscript(id id: Int) -> Book? {
        get { books.first { $0.id == id } }
        set {
            books.removeAll { $0.id == id }
            if let newValue { books.append(newValue) }
        }
    }
}

var lib2 = Library()
lib2[id: 7] = Book(id: 7, title: "CLI Design", author: "You", year: 2025)
lib2[id: 7] = nil
print(lib2.books.count) // 0

Здесь newValue — то, что пытались присвоить. И снова: никаких новых stored properties, только удобный API к имеющемуся books.

Мини-схема контракта subscript

Операция Что пишет пользователь Что делаем внутри
Чтение
lib[id: 7]
Ищем и возвращаем
Book?
Запись
lib[id: 7] = book
Удаляем старую и добавляем новую
Удаление
lib[id: 7] = nil
Удаляем по id

Это «маленькое соглашение» экономит вам десятки строк кода в обработчиках команд CLI.

7. Жёсткий и безопасный subscript

Когда вы проектируете subscript, у вас почти всегда есть развилка. Можно сделать «жёсткий» контракт: индекс всегда корректный, иначе это баг и программа падает (как делает обычный Array при выходе за границы). А можно сделать «мягкий» контракт: вернуть nil и дать вызывающему коду решить, что делать.

В приложениях уровня «учебный CLI» мягкий контракт часто удобнее — меньше падений, больше контроля. Но жёсткий контракт тоже уместен, если вы уверены в логике и хотите заметить баг быстро.

Покажем оба варианта на нашей Library, просто оборачивая доступ к books по индексу.

extension Library {
    subscript(index: Int) -> Book {
        books[index] // ожидаем корректный индекс
    }

    subscript(safeIndex index: Int) -> Book? {
        guard books.indices.contains(index) else { return nil }
        return books[index]
    }
}

Если вы потом пишете lib[999], у вас будет падение (и это иногда полезно в отладке). А если пишете lib[safeIndex: 999], получите nil и сможете показать человеку понятное сообщение вместо аварии.

Как это упрощает CLI-код: до/после

Когда вы пишете команды вроде "show 12" или "remove 12", без subscript получается примерно такой стиль: найти индекс, проверить, удалить. Код длинный, повторяется и отвлекает от смысла команды.

С subscript команда превращается в «читаемую мысль»: «возьми книгу по id», «если нет — скажи», «если есть — напечатай красиво». И computed property вроде displayLine подставляет финальный штрих: вывод стандартизирован.

Пример (демо-кусочек «как будет выглядеть использование»):

var lib = Library(books: [
    Book(id: 1, title: "Swift", author: "Apple", year: 2014)
])

if let book = lib[id: 1] {
    print(book.displayLine) // 1. Swift — Apple (2014)
} else {
    print("Книга не найдена")
}

Обратите внимание: этот код вообще не знает, как именно мы ищем книгу. Сегодня это first, завтра это может быть словарь или индекс — а вызывающий код менять не придётся. И вот это уже похоже на нормальный API-дизайн, а не на «копипасту поискового цикла по проекту».

8. Типичные ошибки при computed properties и subscript в extension

Ошибка №1: пытаться “запомнить” что-то через computed property, как будто это stored property.
Иногда новички пишут computed property и подсознательно ожидают, что значение «где-то хранится». Но computed property вычисляется каждый раз заново. Если вам нужно хранение — это stored property, и его место в основном объявлении типа, а не в extension. В extension хранить состояние нельзя — и это не злость Swift, а защита от хаоса в памяти типа.

Ошибка №2: рекурсия в computed property (getter/setter вызывает сам себя).
Самый частый вариант выглядит невинно: var title: String { get { title } }. Кажется логичным, но на деле getter вызывает getter, и так до бесконечности. Правильный подход — опираться на stored properties (self.someStored) или на приватные методы, но не на то же имя computed property.

Ошибка №3: “тяжёлые вычисления” прячутся за видом простого свойства.
Свойство читается как «достали значение», поэтому мозг не ждёт, что внутри будет сортировка, поиск по огромному массиву или какая-то дорогая логика. Из-за этого API становится обманчивым. Если вычисление заметно по стоимости или по побочным эффектам, лучше сделать метод, чтобы код выглядел как действие, а не как чтение поля. Это согласуется с тем, что разработчики часто ожидают “модель stored property” и не ждут тяжёлой работы при доступе к свойству.

Ошибка №4: subscript без чёткого контракта на “невалидный доступ”.
Если ваш subscript иногда падает, иногда возвращает nil, иногда удаляет элементы при nil, а иногда игнорирует — вы создаёте API-лотерею. До первого выигрыша компилятора (то есть до первого падения в рантайме). Лучше заранее проговорить правило: либо «ожидаем корректный индекс» (как Array), либо «возвращаем optional и не падаем».

Ошибка №5: записывающий subscript делает слишком много “магии”.
Очень соблазнительно сделать set, который и добавляет, и обновляет, и удаляет, и ещё логирует, и ещё синхронизируется с файлом. Но такой subscript становится неожиданным: строка lib[id: 7] = book выглядит простой, а делает полпроекта. Держите subscript коротким и предсказуемым: доступ и минимальная мутация. Всё сложное — в отдельные методы уровня «сервис», но это уже другая история.

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