2.11 方法

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/methods/

2.11 方法

定义和调用属于实例或类型的函数。

方法是与某个特定类型关联的函数。类、结构体和枚举都可以定义实例方法,用来封装处理该类型实例的特定任务与功能;它们也都可以定义类型方法,类型方法与类型本身相关联。类型方法类似 Objective-C 中的类方法。

结构体和枚举在 Swift 中能够定义方法,这是它与 C、Objective-C 的一大区别。在 Objective-C 中,只有类能定义方法;而在 Swift 中,你可以自行选择定义类、结构体还是枚举,同时仍然能在自己创建的类型上灵活地定义方法。

实例方法

实例方法是属于某个特定类、结构体或枚举实例的函数。它们为这些实例提供支持:要么提供访问和修改实例属性的方式,要么提供与实例用途相关的功能。实例方法的语法与函数完全相同,详见函数。

实例方法写在它所属类型的开、闭花括号之内。实例方法可以隐式访问该类型的所有其他实例方法和属性。实例方法只能在它所属类型的某个具体实例上调用;没有已存在的实例,就无法孤立地调用它。

下面是一个定义简单 Counter 类的例子,可以用它统计某个动作发生的次数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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,用来记录计数器的当前值。

调用实例方法时使用与访问属性相同的点语法:

1
2
3
4
5
6
7
8
let counter = Counter()
// 计数器的初始值是 0
counter.increment()
// 计数器的值现在是 1
counter.increment(by: 5)
// 计数器的值现在是 6
counter.reset()
// 计数器的值现在是 0

函数参数既可以有名字(供函数体内部使用),也可以有实参标签(供调用时使用),详见函数实参标签与参数名。方法参数也是如此,因为方法只是与某个类型关联的函数。

self 属性

类型的每个实例都有一个隐式属性 self,它完全等同于实例本身。你在实例方法内部用 self 属性引用当前实例。

上面例子中的 increment() 方法可以写成这样:

1
2
3
func increment() {
    self.count += 1
}

实践中你并不需要经常在代码里写 self。如果不显式写 self,当你在方法内使用某个已知的属性名或方法名时,Swift 会假定你指的是当前实例的属性或方法。Counter 的三个实例方法里使用 count(而不是 self.count)就体现了这一假定。

这条规则的主要例外情况是:实例方法的参数名与实例的某个属性同名。此时参数名优先,于是需要用更限定化的方式引用属性:用 self 属性区分参数名和属性名。

下面这个例子里,self 用来区分名为 x 的方法参数和同样名为 x 的实例属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Point {
    var x = 0.0, y = 0.0
    func isToTheRightOf(x: Double) -> Bool {
        return self.x > x
    }
}
let somePoint = Point(x: 4.0, y: 5.0)
if somePoint.isToTheRightOf(x: 1.0) {
    print("This point is to the right of the line where x == 1.0")
}
// 输出 "This point is to the right of the line where x == 1.0"。

如果没有 self 前缀,Swift 会假定两处 x 都指名为 x 的方法参数。

在实例方法中修改值类型

结构体和枚举是值类型。默认情况下,值类型的属性不能在它的实例方法内部被修改。

不过,如果你需要在某个方法内部修改结构体或枚举的属性,可以为该方法启用可变(mutating)行为。此后,方法就能在内部变更(也就是修改)它的属性,所做的任何修改都会在方法结束时写回原来的结构体;方法也可以给隐式的 self 属性赋一个全新的实例,方法结束时这个新实例会替换原来的实例。

在该方法的 func 关键字前加上 mutating 关键字,即可启用这一行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 关键字,使它能修改自己的属性。

注意,不能在结构体类型的常量上调用可变方法,因为它的属性不能被改变,即使这些属性是变量属性也不行,详见常量结构体实例的存储属性:

1
2
3
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// 这会报错

在可变方法中给 self 赋值

可变方法可以给隐式的 self 属性赋一个全新的实例。上面的 Point 例子也可以写成下面这样:

1
2
3
4
5
6
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 参数设为同一枚举中的另一个成员:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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

这个例子为三态开关定义了一个枚举。每次调用它的 next() 方法,开关就在三个不同的电源状态(off、low 和 high)之间循环。

类型方法

上面介绍的实例方法是在某个类型的实例上调用的方法。你也可以定义在类型本身上调用的方法,这类方法称为类型方法。在方法的 func 关键字前写 static 关键字就表示这是一个类型方法。类可以改用 class 关键字,以便子类覆盖父类对该方法的实现。

注意:在 Objective-C 中,你只能为 Objective-C 类定义类型层面的方法。 在 Swift 中,你可以为所有类、结构体和枚举定义类型层面的方法。 每个类型方法都明确地限定在它所支持的那个类型上。

类型方法用点语法调用,和实例方法一样,只不过是在类型上调用,而不是在该类型的实例上调用。下面演示如何在名为 SomeClass 的类上调用类型方法:

1
2
3
4
5
6
class SomeClass {
    class func someTypeMethod() {
        // 类型方法的实现写在这里
    }
}
SomeClass.someTypeMethod()

在类型方法的语句体内,隐式的 self 属性指向类型本身,而不是该类型的实例。这意味着你可以用 self 在类型属性与类型方法参数之间消除歧义,就像在实例属性和实例方法参数之间那样。

更一般地说,在类型方法体内使用的任何不带限定的方法名和属性名,都会指向其他类型层面的方法和属性。类型方法可以直接用另一个类型方法的名字调用它,而无需加上类型名前缀;同样,结构体和枚举上的类型方法访问类型属性时也可以直接使用属性名,而无需加类型名前缀。

下面的例子定义了一个名为 LevelTracker 的结构体,用来跟踪玩家在一个游戏各关卡(阶段)中的进度。这是一个单机游戏,但可以在同一台设备上存储多个玩家的信息。

游戏首次运行时,除了第一关之外的所有关卡都是锁定的。每当有玩家通过某一关,该关卡就会对这台设备上的所有玩家解锁。LevelTracker 结构体用类型属性和类型方法来跟踪游戏已解锁的关卡,同时跟踪单个玩家的当前关卡。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 特性。关于这个特性的更多内容,参见属性特性。

LevelTracker 结构体与下面显示的 Player 类配合使用,用于跟踪和更新单个玩家的进度:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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(_:) 调用已经确定该关卡被解锁了。)

你可以为新玩家创建一个 Player 类实例,看看玩家通过第一关时会发生什么:

1
2
3
4
var player = Player(name: "Argyrios")
player.complete(level: 1)
print("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
// 输出 "highest unlocked level is now 2"。

如果你创建第二个玩家并试图把他移动到一个尚未被任何玩家解锁的关卡,那么设置该玩家当前关卡的尝试会失败:

1
2
3
4
5
6
7
player = Player(name: "Beto")
if player.tracker.advance(to: 6) {
    print("player is now on level 6")
} else {
    print("level 6 hasn't yet been unlocked")
}
// 输出 "level 6 hasn't yet been unlocked"。