Основы Swift / 12.1. Методы


Видео


Методы
Методы - это функции, которые связаны с конкретным типом. Классы, структуры и перечисления могут определять методы экземпляров, которые инкапсулируют определённые задачи и функциональность для работы с объектом данного типа. Классы, структуры и перечисления также могут определять методы типа, которые ассоциируются с типом в целом.
Методы экземпляров
Методы экземпляров - это функции, которые принадлежат экземплярам отдельного класса, структуры или перечисления. Они поддерживают функционал данных объектов, или предоставлением путей доступа и модификации свойств объекта, или предоставляя функционал, связанный с целью объекта. Методы экземпляров имеет тот же синтаксис, что и функции.

Вы можете написать метод объекта внутри фигурных скобок типа, которому он принадлежит. Метод объекта имеет неявный доступ ко всем остальным методам объекта и свойствам этого типа. Метод объекта может быть вызван только на конкретном экземпляре типа, которому он принадлежит. Он не может быть вызван в изоляции от существующего объекта.

Здесь дан пример, определяющий класс Counter, который может быть использован для подсчёта числа того, сколько раз произошло событие:
@{12.1\1\1}
class Counter {
    var count = 0
    func increment() {
        count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}
Класс Counter определяет три метода объекта:
  • increment() увеличивает счётчик на 1.
  • increment(by: Int) увеличивает счётчик на указанное целое число.
  • reset() сбрасывает счётчик в нуль.
Класс Counter также определяет переменное свойство, count, для отслеживания текущего значения счётчика.
Точечный синтаксис
Вы можете вызвать методы экземпляра с тем же точечным синтаксисом, что и свойства:
@{12.1\1\2}
let counter = Counter()
// начальное значение счётчика равно 0
counter.increment()
// значение счэтчика теперь равно 1
counter.increment(by: 5)
// значение счэтчика теперь равно 6
counter.reset()
// значение счэтчика теперь равно 0
Параметры функции имеют как имя (для использования внутри тела функции), так и ярлык аргумента (для использования при вызове функции). То же верно и для параметров методов, так как методы это просто функции, ассоциированные с типом.
Свойство self
Каждый экземпляр типа имеет неявное свойство с названием self, которое является на деле эквивалентом для самого этого экземпляра. Вы можете использовать свойство self для ссылки на текущий объект ищ его собственных методов.

Метод increment() в примере выше может быть переписан так:
@{12.1\2}
class Counter {
    var count = 0
    func increment() {
        self.count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}
На практике Вам не нужно писать self в Вашем коде слишком часто. Если Вы не напишите явно self, то Swift примет, что Вы ссылаетесь на свойство или метод текущего объекта, когда вы используете известное имя свойства или метода внутри метода. Это демонстрируется с помощью использования count (вместо self.count) внутри трёх методов экземпляра для Counter.

Главное исключение для этого правила происходит, когда имя параметра для метода объекта имеет то же имя, что свойство объекта. В этой ситуации имя параметра имеет приоритет, а потому становится необходимым сослаться на свойство в более квалифицированном способе. Вы можете использовать свойство self, чтобы разбить неопределённость между именем параметра и именем свойства.
@{12.1\3}
struct  Point {
    var x = 0.0, y = 0.0
    func isToTheRightOfX(x: Double) -> Bool {
        return self.x > x
    }
}

let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOfX(x: 1.0) {
    print("This point is to the right of the line where x == 1.0")
}
// Выведет print("This point is to the right of the line where x == 1.0")
Здесь self снимает неопределённость между параметром метода с названием x и свойством объекта с таким же именем x.

Без префикса self Swift может принять, что оба использования x ссылаются на параметр метода x.
Изменение типов-значений изнутри методов экземпляра
Структуры и перечисления - это типы-значения. По умолчанию, свойства типа-значения не могут быть изменены из его методов-экземпляров.

Однако, если Вам нужно изменить свойства Вашей структуры или перечисления отдельным методом, Вы можете добиться мутирующего поведения для этого метода. Метод может мутировать (что значит, изменять) свойства изнутри, и любые изменения, что он сделает, будут записаны в изначальную структуру, когда метод завершит своё выполнение. Метод также может присвоить полночью новый объект неявному свойству self, и этот новый экземпляр заменит существующий, когда метод завершит свою работу.

Вы можете добиться этого поведения, разместив ключевое слово mutating перед ключевым словом func для этого метода:
@{12.1\4\1}
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX
        y += deltaY
    }
}
var somePoint = Point(x: 1.0, y: 1.0)
somePoint.moveBy(x: 2.0, y: 3.0)
print("The point is now at (\(somePoint.x), \(somePoint.y))")
// Выведет "The point is now at (3.0, 4.0)
Структура Point выше определяет мутирующий метод moveBy(x:y:), который перемещает объект типа Point на указанное расстояние. Вместо возвращения новой точки этот метод на деле модифицирует точку, на которой он вызван. Ключевое слово mutating добавлено в определение для включения возможности модифицировать свойства.

Заметьте, что Вы не можете вызвать мутирующий метод на константе типа-структуры, так как его свойства не могут быть изменены, даже если это переменные свойства.
@{12.1\4\2}
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// это вызовет ошибку
Присвоение self изнутри мутирующего метода
Мутирующие методы могут присвоить полностью новый объект неявному свойству self. Пример Point, данный выше, может быть записан в следующем виде:
@{12.1\5}
struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        self = Point(x: x + deltaX, y: y + deltaY)
    }
}
Эта версия мутирующего метода moveBy(x:y:) создаёт полностью новую структуру, чьи значения x и y устанавливаются в целевую локацию. Конечный результат вызова альтернативной версии этого метода будет таким же, как и вызов ранней версии.
Мутирующие методы для перечислений
Мутирующие методы для перечислений могут установить неявный параметр self быть другим кейсом для того же перечисления:
@{12.1\6}
enum TriStateSwitch {
    case off, low, high
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}
var ovenLight = TriStateSwitch.low
ovenLight.next()
// ovenLight теперь равен .high
ovenLight.next()
// ovenLight теперь равен .off
Этот пример определяет перечисление для трёхшагового переключателя. Переключатель циркулирует между трёх разных состояний энергии (off, low и high) всякий раз, когда вызывается метод next().
Методы типа
Методы экземпляров, как описано выше, это методы, которые вызываются на объекте заданного типа. Вы также можете задать методы, которые вызываются на типе в целом. Эти виды методов называются методами типа. Вы можете индицировать методы типа, написав ключевое слово static перед ключевым словом метода func.

Методы типа вызываются с использованием точечного синтаксиса, как и методы объекта. Однако Вы можете вызвать метод типа на самом типа, а не на объекте данного типа. Здесь указан пример того, как Вы можете вызвать метод на классе с названием SomeClass:
@{12.1\7}
class SomeClass {
    class func someTypeMethod() {
        // здесь располагается реализация метода типа
    }
}
SomeClass.someTypeMethod()
Внутри тела метода типа неявно свойство self ссылается на тип в общем, а не на отдельный экземпляр этого типа. Это означает, что Вы можете использовать self для снятия неопределённости между свойствами типа и параметрами метода типа, как если бы Вы работали со свойствами объекта и параметрами метода объекта.

Более обобщённо говоря, любое неквалифицированное имя метода и свойства, которые Вы используете внутри тела метода типа будет ссылаться на другие методы или свойства уровня типа. Метод типа может вызвать другой метод типа с помощью его имени без необходимости предварять его именем типа. Аналогично, методы типа на структурах и перечислениях могут получить доступ к свойствам типа, используя имя свойства типа без префиксирования именем типа.
Моделирование трекинга уровней
Пример ниже определяет структуру с названием LevelTracker, которая отслеживает прогресс игрока между разными уровнями или стадиями игры. Это синглплеерная игра, но может хранить информацию для нескольких игроков на одном устройстве.

Все уровни игры (кроме уровня один) заблокированы, когда игра запускается впервые. Всякий раз, когда игрок завершает уровень, этот уровень разблокируется для всех игроков на устройстве. Структура LevelTracker использует свойства и методы типа для отслеживания того, какие уровни в игре были разблокированы. Она также отслеживает текущий уровень для отдельного игрока.
@{12.1\8\1}
struct LevelTracker {
    static var highestUnlockedLevel = 1
    var currentLevel = 1

    static func unlock(_ level: Int) {
        if level > highestUnlockedLevel { highestUnlockedLevel = level }
    }

    static func isUnlocked(_ level: Int) -> Bool {
        return level <= highestUnlockedLevel
    }

    @discardableResult
    mutating func advance(to level: Int) -> Bool {
        if LevelTracker.isUnlocked(level) {
            currentLevel = level
            return true
        } else {
            return false
        }
    }
}
Структура LevelTracker отслеживает наивысший уровень, который был разблокирован игроком. Это значение хранится в свойстве типа с названием highestUnlockedLevel.

LevelTracker также определяет две функции типа для работы со свойством highestUnlockedLevel. Первая имеет название unlock(_:) и обновляет значение highestUnlockedLevel, всякий раз при разблокировке нового уровня. Вторая - это удобная функция типа с названием isUnlocked(_:), которая возвращает true, если номер заданного уровня уже разблокирован. (Обратите внимание, что эти методы типа могут получить доступ к свойству типа highestUnlockedLevel без необходимости записывать его в виде LevelTracker.highestUnlockedLevel.)

В дополнение к свойству типа методам типа LevelTracker отслеживает прогресс отдельного игрока через игру. Он использует свойство объекта с названием currentLevel для отслеживания текущего уровня игрока.

Чтобы помочь управлять свойством currentLevel, LevelTracker определяет метод объекта с названием advance(to:). Прежде, чем обновить currentLevel, этот метод проверяет, разблокирован ли уже новый запрашиваемый уровень. Метод advance(to:) возвращает Булево значение для указания того, можно ли или нет установить currentLevel. Так как это не необходимо, ошибка для кода, что вызывает advance(to:) метод, игнорировать возвращаемое значение, то функция помечена атрибутом @discardableResult.
@{12.1\8\2}
Структура LevelTracker испольузется в классе Player, данном ниже, для отслеживания и обновления прогресса отдельного игрока:
class Player {
    var tracker = LevelTracker()
    let playerName: String
    func complete(level: Int) {
        LevelTracker.unlock(level + 1)
        tracker.advance(to: level + 1)
    }
    init(name: String) {
        playerName = name
    }
}
Класс Player создаёт новый объект типа LevelTracker для отслеживания прогресса игрока. Он также предоставляет метод с именем complete(level:), который вызывается, как только игрок завершает определённый уровень. Этот метод разблокирует следующий уровень для всех игроков и обновляет прогресс игрока для пресыщения их к новому уровню. (Булево возвращаемое значение advance(to:) игнорируется, такая уровень известен, что он разблокирован вызовом LevelTracker.unlock(_:) на предыдущей строке.)
@{12.1\8\3}
Вы можете создать объект типа Player для нового игрока и увидеть, что происходит, когда игрок завершает уровень один:
var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// Выведет "highest unlocked level is now 2"
@{12.1\8\4}
Если Вы создадите второго игрока, которого Вы пытаетесь передвинуть на уровень, который ещё не разблокирован никаким игроком в игре, то попытка установить значение текущего уровня провалится:
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
    print("player is now on level 6")
} else {
    print("level 6 has not yet been unlocked")
}
// Выведет "level 6 has not yet been unlocked"