$ sudo teach IT

Зачем данным свои действия

Вы уже умеете хранить данные в структурах и классах: свойства лежат внутри, а работу с ними пишете снаружи, отдельными функциями. Это работает, но неудобно масштабировать: функция 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 — создаёт сетку указанного размера, где все ячейки равны нулю.