Практика 3. Инициализация и жизненный цикл объектов
Цель
Научиться проектировать безопасные пути создания значений и объектов, устранять
дублирование между инициализаторами, правильно инициализировать иерархию классов,
применять lazy и контролировать освобождение ресурсов.
Общие требования
- выполняйте задания в отдельных файлах
task_N.swift; - все примеры должны компилироваться Swift 6;
- некорректный экземпляр не должен появляться в программе;
- дополнительные инициализаторы должны делегировать основному;
- добавьте
assertдля успешных и неуспешных сценариев; - не вызывайте
deinitвручную — это невозможно и концептуально неверно.
Задание 1. Проверяемая модель UserAccount
Создайте неизменяемую структуру учётной записи.
struct UserAccount: Equatable { let username: String let email: String let age: Int
init?(username: String, email: String, age: Int) { fatalError("TODO") }}Инварианты:
- имя после удаления пробелов содержит не менее трёх символов;
- email содержит одну
@и непустые части до и после неё; - возраст находится в диапазоне
14...120; - сохраняются нормализованные значения без пробелов по краям;
- email приводится к нижнему регистру.
assert(UserAccount(username: " amina ", email: " USER@EXAMPLE.COM ", age: 20) == UserAccount(username: "amina", email: "user@example.com", age: 20))assert(UserAccount(username: "ab", email: "a@b.com", age: 20) == nil)assert(UserAccount(username: "amina", email: "wrong", age: 20) == nil)assert(UserAccount(username: "amina", email: "a@b.com", age: 10) == nil)Не создавайте объект с временно некорректными полями и не исправляйте его после инициализации.
Задание 2. Несколько способов создания Temperature
Реализуйте значение температуры с единым основным путём инициализации.
struct Temperature: Equatable { let celsius: Double
init?(celsius: Double) { fatalError("TODO") }
init?(fahrenheit: Double) { fatalError("TODO") }
init?(kelvin: Double) { fatalError("TODO") }}Требования:
- температура не может быть ниже
-273.15 °C; - инициализаторы Фаренгейта и Кельвина переводят значение и делегируют через
self.init(celsius:); - проверки абсолютного нуля не дублируются;
- допускается небольшая погрешность при сравнении результатов вычислений.
let freezingC = Temperature(celsius: 0)let freezingF = Temperature(fahrenheit: 32)let freezingK = Temperature(kelvin: 273.15)
assert(freezingC != nil)assert(abs((freezingF?.celsius ?? 1) - 0) < 0.0001)assert(abs((freezingK?.celsius ?? 1) - 0) < 0.0001)assert(Temperature(kelvin: -1) == nil)Задание 3. Designated и convenience initializer
Создайте класс документа с одним designated initializer и двумя convenience initializer.
import Foundation
final class Document { let id: UUID let title: String private(set) var body: String let createdAt: Date
init(id: UUID, title: String, body: String, createdAt: Date) { fatalError("TODO") }
convenience init(title: String, body: String) { fatalError("TODO") }
convenience init(title: String) { fatalError("TODO") }
func updateBody(_ newBody: String) { fatalError("TODO") }}Требования:
- designated initializer получает все значения;
- первый convenience initializer генерирует
UUID()иDate(); - второй создаёт документ с пустым телом через другой convenience initializer;
- заголовок после удаления пробелов не должен быть пустым;
- если заголовок некорректен, используйте осмысленное безопасное решение:
измените все инициализаторы на проваливающиеся либо нормализуйте заголовок к
заранее документированному значению
"Без названия"; - выбранное решение объясните.
Проверьте, что оба удобных пути в итоге создают полностью заполненный объект.
Задание 4. Инициализация иерархии транспорта
Дополните классы, соблюдая порядок первой фазы инициализации.
class Vehicle { let identifier: String private(set) var mileage: Int
init?(identifier: String, mileage: Int = 0) { fatalError("TODO") }
func drive(kilometers: Int) -> Bool { fatalError("TODO") }}
final class ElectricCar: Vehicle { let batteryCapacity: Int private(set) var chargeLevel: Int
init?( identifier: String, mileage: Int = 0, batteryCapacity: Int, chargeLevel: Int = 100 ) { fatalError("TODO") }
convenience init?(identifier: String, batteryCapacity: Int) { fatalError("TODO") }
func charge() { fatalError("TODO") }}Инварианты:
- идентификатор непустой;
- пробег неотрицательный;
- ёмкость батареи положительная;
- заряд находится в диапазоне
0...100; - подкласс присваивает собственные свойства до
super.init; - convenience initializer делегирует designated initializer своего класса.
Добавьте проверки для каждой границы и письменно опишите порядок инициализации
ElectricCar.
Задание 5. Ленивое построение отчёта
Создайте класс, который выполняет дорогую операцию только при первом обращении.
final class SalesReport { let values: [Double] private(set) var buildCount = 0
lazy var formatted: String = { fatalError("TODO") }()
init(values: [Double]) { self.values = values }}Строка должна содержать сумму, среднее и число элементов. Внутри замыкания
увеличьте buildCount.
let report = SalesReport(values: [10, 20, 30])assert(report.buildCount == 0)
let firstResult = report.formattedassert(report.buildCount == 1)
let secondResult = report.formattedassert(report.buildCount == 1)assert(firstResult == secondResult)Объясните, почему formatted должно быть var, даже если после первого
вычисления результат больше не меняется.
Задание 6. Явное и резервное освобождение ресурса
Реализуйте имитацию соединения. Реальных сетевых запросов не требуется.
final class Connection { let name: String private(set) var isOpen: Bool private let onClose: (String) -> Void
init(name: String, onClose: @escaping (String) -> Void) { fatalError("TODO") }
func send(_ message: String) -> Bool { fatalError("TODO") }
func close() { fatalError("TODO") }
deinit { close() }}Требования:
- после создания соединение открыто;
- пустое сообщение и отправка после закрытия возвращают
false; close()идемпотентен: повторный вызов не вызываетonCloseснова;deinitвызываетclose()как страховку;- тест фиксирует количество закрытий без привязки к точной строке выполнения
deinit.
var closedNames: [String] = []
do { let connection = Connection(name: "api") { closedNames.append($0) } assert(connection.send("ping")) connection.close() connection.close() assert(closedNames == ["api"])}
assert(closedNames == ["api"])Затем повторите сценарий без явного close() и проверьте после завершения
области do, что резервная очистка произошла один раз.
Дополнительное задание. required init
Создайте базовый класс Command с required init(name:) и два подкласса. Каждый
подкласс должен корректно реализовать обязательный инициализатор и установить
собственные значения по умолчанию. Создайте функцию, которая принимает
Command.Type и создаёт команду через обязательный инициализатор.
func makeCommand(_ type: Command.Type, name: String) -> Command { type.init(name: name)}Объясните, почему без required компилятор не мог бы гарантировать допустимость
такого создания для любого подкласса.
Критерии оценки
| Раздел | Доля |
|---|---|
Задания 1–2: проверка данных и делегирование self.init | 25% |
| Задание 3: designated/convenience initializer | 15% |
| Задание 4: корректная инициализация иерархии | 20% |
Задание 5: корректное использование lazy | 15% |
| Задание 6: жизненный цикл и идемпотентная очистка | 15% |
| Качество кода, проверки и объяснения | 10% |
Дополнительное задание может компенсировать до 10% потерянных баллов.
Вопросы для самопроверки
- Почему все хранимые свойства должны получить значения до завершения
init? - В каком случае удобен
init?? - Как избежать дублирования между несколькими инициализаторами структуры?
- Чем designated initializer отличается от convenience initializer?
- Почему подкласс присваивает свои свойства до
super.init? - Что означает двухфазная инициализация?
- Когда вычисляется
lazy-свойство? - Что определяет время жизни экземпляра класса?
- Почему
close()должен быть идемпотентным? - Почему очистку важного ресурса не стоит оставлять только на
deinit?