Зачем данным свои действия
Вы уже умеете хранить данные в структурах и классах: свойства лежат внутри, а работу с ними
пишете снаружи, отдельными функциями. Это работает, но неудобно масштабировать: функция
describe(player) может жить где угодно, а её легко потерять среди сотен других
функций проекта. Гораздо естественнее, когда действие, которое умеет делать значение, лежит
рядом с самим значением: не «функция, которая работает с игроком», а «то, что игрок умеет
делать». Для этого в Swift есть методы — функции, объявленные прямо внутри struct,
class или (позже) enum.
Методы экземпляра и self
Метод — это обычная функция, но объявленная внутри тела типа. Она автоматически получает доступ ко всем свойствам этого типа, без передачи их параметрами.
struct Player {
var name: String
var health: Int
func describe() -> String {
return "\(name): \(health) HP"
}
}
let hero = Player(name: "Ари", health: 80)
print(hero.describe())
Внутри describe() имена name и health сами находят
нужные свойства текущего экземпляра — того, для которого метод вызвали через точку:
hero.describe(). На этот экземпляр внутри метода можно сослаться явно словом
self — это то же самое, что name, только записанное как
self.name. Компилятор в большинстве случаев сам понимает, что вы имеете в виду
свойство, а не что-то другое, поэтому self. обычно опускают. Оно обязательно
только тогда, когда параметр метода называется так же, как свойство, и без self
их было бы не различить:
struct Counter {
var value: Int
mutating func set(value: Int) {
self.value = value
}
}
Здесь value без уточнения — это параметр, а self.value — свойство
структуры. Без self строка value = value присвоила бы параметру самому
себе и ничего бы не изменила.
mutating: структурам нужно разрешение на изменение
struct — тип значения, и по умолчанию Swift считает методы структуры
только читающими: они могут смотреть на свойства, но не менять их. Это защита от случайных
изменений и одновременно честное предупреждение компилятора: «этот метод трогает данные».
Если методу нужно поменять свойство, перед func ставится слово
mutating:
struct Lamp {
var isOn = false
mutating func toggle() {
isOn.toggle()
}
}
var lamp = Lamp()
lamp.toggle()
print(lamp.isOn)
Без mutating строка isOn.toggle() вообще не скомпилируется:
компилятор запретит менять свойство внутри не помеченного метода. Ещё одно следствие —
вызвать mutating метод можно только на переменной (var), а не на
константе (let): let запрещает менять экземпляр целиком, а
mutating метод как раз это и делает. У классов такой проблемы нет: класс — ссылочный
тип, и метод меняет данные по той же ссылке независимо от того, объявлена переменная класса как
var или let, поэтому слово mutating для методов класса не
пишут вообще.
mutating метод может не только менять отдельные свойства, но и целиком заменить
себя новым значением через self = ...:
struct Position {
var x: Int
var y: Int
mutating func moveToOrigin() {
self = Position(x: 0, y: 0)
}
}
Свои subscript: доступ по индексу
Обращение array[2] или dictionary["key"] — это не магия языка,
а обычный вызов метода особого вида, который называется subscript. Такой же
можно объявить и у собственного типа, чтобы обращаться к нему через квадратные скобки вместо
обычного метода:
struct Playlist {
var tracks: [String]
subscript(index: Int) -> String {
get {
return tracks[index]
}
set {
tracks[index] = newValue
}
}
}
var list = Playlist(tracks: ["Утро", "День"])
list[1] = "Вечер"
print(list[1])
subscript объявляется без слова func и без имени, параметры пишутся
в квадратных скобках, а тип после стрелки — это то, что возвращает чтение. Устроен он как
вычисляемое свойство: блок get отвечает за чтение, блок set — за запись,
а внутри set новое значение приходит в переменной newValue. Если
запись не нужна, блок set просто не пишут, и subscript становится
доступным только для чтения. Обратите внимание: для структуры set у
subscript меняет свойство tracks, но слово mutating
перед самим subscript писать не нужно — компилятор понимает, что раз есть
set, то доступ к этому индексу может менять экземпляр, и добавляет это правило сам,
как для обычного вычисляемого свойства.
static: методы уровня типа
Иногда действие относится не к конкретному игроку или счётчику, а к самому типу в целом:
например, способ построить значение из данных другого формата. Для этого есть
static func — метод, который вызывается не через экземпляр, а прямо через имя типа:
struct Temperature {
var celsius: Double
static func fromFahrenheit(_ f: Double) -> Temperature {
return Temperature(celsius: (f - 32) / 1.8)
}
}
let boiling = Temperature.fromFahrenheit(212)
print(boiling.celsius)
Внутри static метода нет конкретного экземпляра, поэтому там нет и
self, указывающего на объект: self в статическом контексте означает
сам тип (Temperature), а не значение с конкретным celsius. Такой метод
не может напрямую прочитать обычное (не статическое) свойство — ему просто неоткуда его взять,
ведь экземпляров может быть сколько угодно или ни одного. Чаще всего static func
используют как альтернативный способ создать значение — «фабричный метод» с говорящим именем,
когда обычного инициализатора недостаточно, чтобы описать намерение вызывающего кода.
Частые ошибки
- Забыть
mutatingу метода структуры, который меняет свойство: компилятор откажется компилировать код с сообщением про то, чтоselfнеизменяемый. - Пытаться вызвать
mutatingметод на структуре, объявленной черезlet: даже если сам метод написан верно, вызывающий код не скомпилируется. - Писать
self.value = valueтам, где имена параметра и свойства не совпадают — это просто лишний код, но не ошибка. А вот пропуститьself.там, где имена совпадают, — уже ошибка логики: присваивание ничего не изменит. - Ожидать, что
staticметод увидит свойства какого-то одного экземпляра. У статического метода нет привязки ни к одному конкретному значению типа. - Забыть блок
setуsubscript, а потом пытаться писать в него значение — компилятор справедливо откажет, ведь такойsubscriptобъявлен только для чтения.
Итоги
- Метод — функция, объявленная внутри типа; она видит его свойства без передачи параметрами.
self— ссылка на текущий экземпляр внутри метода; обязательна только при совпадении имён параметра и свойства.- Методы структур по умолчанию не могут менять свойства; для этого перед
funcставитсяmutating. mutatingметод можно вызывать только уvar, а не уlet.subscriptдобавляет типу доступ через квадратные скобки, устроен как вычисляемое свойство с блокамиgetи необязательнымset.static func— метод уровня типа, вызывается через имя типа и не видит свойства конкретного экземпляра; удобен как фабричный способ создания значений.
Проверьте себя
3 вопроса
Счётчик очков
Реализуйте структуру ScoreCounter — счётчик очков игрока.
У структуры уже есть хранимое свойство score: Int. Допишите три метода:
mutating func add(_ points: Int)— прибавляетpointsк счёту (число может быть и отрицательным — очки можно и списать). Если после прибавления счёт оказался меньше нуля, он должен стать ровно нулём (уходить в минус нельзя).func current() -> Int— возвращает текущее значение счёта.static func makeStarting(at points: Int) -> ScoreCounter— создаёт новый счётчик с начальным значениемpoints; еслиpointsотрицательное, начальное значение тоже должно стать нулём.
Сетка чисел
Реализуйте структуру Grid — прямоугольную сетку целых чисел из rows строк и cols столбцов, с доступом к ячейкам по двум индексам сразу, как grid[row, col].
У структуры уже объявлены свойства rows, cols и values (плоский массив длиной rows * cols, в котором ячейка строки row и столбца col лежит по индексу row * cols + col). Допишите:
subscript(row: Int, col: Int) -> Int— чтение и запись значения нужной ячейки черезvalues.static func zero(rows: Int, cols: Int) -> Grid— создаёт сетку указанного размера, где все ячейки равны нулю.