Перейти к содержимому

Практика 2. Структуры, классы и копирование

Цель

Научиться осознанно выбирать между struct и class, прогнозировать результат копирования, управлять изменяемостью значимых типов и находить неочевидное общее состояние в смешанных моделях.


Общие требования

  • каждое задание выполняйте в отдельном файле task_N.swift;
  • добавляйте проверки assert;
  • структуры, сравниваемые по данным, делайте Equatable;
  • классы без предполагаемого наследования объявляйте final;
  • после каждого задания кратко объясняйте выбор struct или class.

Задание 1. Значимый тип Vector2D

Реализуйте двумерный вектор как структуру.

Требования:

  • свойства x и y типа Double;
  • вычисляемое свойство length;
  • mutating func scale(by:), отклоняющий неположительный коэффициент;
  • func scaled(by:) возвращает новую копию и не меняет исходную;
  • автоматическое сравнение через Equatable.
struct Vector2D: Equatable {
var x: Double
var y: Double
var length: Double {
fatalError("TODO")
}
@discardableResult
mutating func scale(by factor: Double) -> Bool {
fatalError("TODO")
}
func scaled(by factor: Double) -> Vector2D? {
fatalError("TODO")
}
}

Проверьте, что изменение копии не влияет на оригинал.

var original = Vector2D(x: 3, y: 4)
var copy = original
copy.scale(by: 2)
assert(original == Vector2D(x: 3, y: 4))
assert(copy == Vector2D(x: 6, y: 8))
assert(original.length == 5)

Задание 2. Неизменяемые значения предметной области

Создайте структуры EmailAddress и StudentRecord.

struct EmailAddress: Equatable {
let value: String
init?(_ value: String) {
fatalError("TODO")
}
}
struct StudentRecord: Equatable {
let id: Int
let name: String
let email: EmailAddress
var grades: [Int]
var averageGrade: Double {
fatalError("TODO")
}
func addingGrade(_ grade: Int) -> StudentRecord? {
fatalError("TODO")
}
}

Требования:

  • email непустой и содержит ровно одну @, до и после неё есть символы;
  • оценки находятся в диапазоне 1...5;
  • addingGrade(_:) возвращает изменённую копию либо nil;
  • исходная запись после вызова не меняется;
  • среднее пустого списка равно 0.

Не превращайте StudentRecord в класс. Объясните, почему учебная запись в этом задании удобна как снимок значения.


Задание 3. Ссылочный тип Playlist

Создайте плейлист, который может совместно использоваться несколькими частями программы.

final class Playlist {
let id: Int
var name: String
private(set) var tracks: [String] = []
init(id: Int, name: String) {
self.id = id
self.name = name
}
@discardableResult
func add(_ track: String) -> Bool {
fatalError("TODO")
}
@discardableResult
func remove(_ track: String) -> Bool {
fatalError("TODO")
}
}

Пустые названия и дубликаты добавлять нельзя. Покажите ссылочную семантику:

let playlist = Playlist(id: 1, name: "Учёба")
let sharedPlaylist = playlist
playlist.add("Swift Basics")
assert(sharedPlaylist.tracks == ["Swift Basics"])
assert(playlist === sharedPlaylist)
let anotherPlaylist = Playlist(id: 1, name: "Учёба")
assert(playlist !== anotherPlaylist)

Письменно ответьте, почему одинаковые id и name не делают два экземпляра тождественными.


Задание 4. Найдите скрытое общее состояние

Дан код:

final class Address {
var city: String
init(city: String) {
self.city = city
}
}
struct OrderDraft {
var number: Int
var address: Address
}
let address = Address(city: "Нальчик")
var first = OrderDraft(number: 1, address: address)
var second = first
second.number = 2
second.address.city = "Москва"

Выполните три шага:

  1. До запуска предскажите значения first.number, second.number, first.address.city и second.address.city.
  2. Подтвердите прогноз через assert.
  3. Переделайте Address в структуру так, чтобы черновики были полностью независимы. Добавьте тест, подтверждающий исправление.

Объясните, почему одной надписи struct у OrderDraft было недостаточно для полностью независимого копирования модели.


Задание 5. Смешанная модель интернет-заказа

Спроектируйте модель из значимых и ссылочных типов.

import Foundation
struct Money: Equatable {
let amount: Decimal
let currency: String
init?(amount: Decimal, currency: String) {
fatalError("TODO")
}
}
struct Product: Equatable {
let id: UUID
let name: String
let price: Money
init?(id: UUID = UUID(), name: String, price: Money) {
fatalError("TODO")
}
}
struct OrderItem: Equatable {
let product: Product
var quantity: Int
init?(product: Product, quantity: Int) {
fatalError("TODO")
}
var subtotal: Decimal {
fatalError("TODO")
}
}
final class ShoppingCart {
let id: UUID
private(set) var items: [OrderItem] = []
var total: Decimal {
fatalError("TODO")
}
init(id: UUID = UUID()) {
self.id = id
}
@discardableResult
func add(_ product: Product, quantity: Int = 1) -> Bool {
fatalError("TODO")
}
@discardableResult
func remove(productID: UUID) -> Bool {
fatalError("TODO")
}
}

Инварианты:

  • сумма денег неотрицательна, код валюты непустой;
  • имя товара непустое;
  • количество положительное;
  • повторное добавление товара увеличивает количество существующей позиции;
  • total всегда вычисляется из текущих позиций.

В конце создайте две ссылки на одну корзину и докажите через === и assert, что они видят общий список. Затем создайте вторую корзину с такими же товарами и покажите, что это другой экземпляр.


Задание 6. Эксперимент с Array и copy-on-write

Создайте массив из миллиона целых чисел, присвойте его второй переменной и измените только второй массив.

var firstArray = Array(0..<1_000_000)
var secondArray = firstArray
secondArray.append(1_000_000)
assert(firstArray.count == 1_000_000)
assert(secondArray.count == 1_000_001)

Ответьте письменно:

  1. Почему первый массив не изменился?
  2. Обязан ли Swift физически копировать миллион элементов в момент присваивания?
  3. В какой момент реализация может создать отдельный буфер?
  4. Почему copy-on-write не превращает Array в ссылочный тип?

Критерии оценки

РазделДоля
Задания 1–2: значимые типы и безопасное копирование25%
Задание 3: ссылочная семантика и идентичность15%
Задание 4: анализ смешанной модели20%
Задание 5: проектирование модели заказа25%
Задание 6 и качество объяснений15%

Работа засчитывается при наборе не менее 60%.


Вопросы для самопроверки

  1. Почему изменяющий метод структуры требует mutating?
  2. Чем копирование структуры отличается от присваивания класса?
  3. Как let влияет на структуру и на ссылку на класс?
  4. Может ли структура содержать класс? Как это влияет на копирование?
  5. Чем == отличается от ===?
  6. Почему Array остаётся значимым типом при copy-on-write?
  7. Какие сущности модели заказа получили тип struct и почему?
  8. Почему корзина реализована классом?