离线下载
PDF版 ePub版

极客学院团队出品 · 更新于 2018-11-28 11:00:42

方法 - Methods

方法是与特定类型相关联的函数。类、结构体以及枚举均可以定义实例方法,该方法为指定类型的实例封装了特定的任务与功能。类、结构体以及枚举也能定义类型方法,该方法与类型自身相关联。类型方法类似于在 Objective-C 中的类方法。

在 Swift 中,结构体和枚举能够定义方法;事实上这是 Swift 与 C/Objective-C 的主要区别之一。在 Objective-C 中,类是唯一能定义方法的类型。在 Swift 中,你可以选择是否定义一个类、结构体或枚举,且仍可以灵活地对你所创建的类型进行方法的定义。

实例方法

实例方法是某个特定类、结构体或枚举类型的实例的方法。他们通过提供访问的方式和修改实例属性,或提供与实例目的相关的功能性来支持这些实例的功能性。准确的来讲,实例方法的语法与函数完全一致,参考函数说明。

实例方法要写在它所属的类型的前后括号之间。实例方法能够访问他所属类型的所有的其他实例方法和属性。实例方法只能被它所属的类的特定实例调用。实例方法不能被孤立于现存的实例而被调用。

下面定义一个简单的类 Counter 的示例(Counter 可以用来对一个动作发生的次数进行计数):

    class Counter {
        var count = 0
        func increment() {
            count++
        }
        func incrementBy(amount: Int) {
            count += amount
        }
        func reset() {
            count = 0
        }
    }

Counter 可以定义三种实例方法:

  • increment 让计数器按一递增
  • incrementBy(amount: Int) 让计数器按一个指定的整数值递增
  • reset 将计数器重置为 0

Counter 这个类还声明了一个可变属性 count,用它来保持对当前计数器值的追踪。

和调用属性一样,用点语法调用实例方法:

    let counter = Counter()
    // the initial counter value is 0
    counter.increment()
    // the counter's value is now 1
    counter.incrementBy(5)
    // the counter's value is now 6
    counter.reset()
    // the counter's value is now 0

方法的局部参数名称和外部参数名称

函数参数有一个局部名称(在函数体内部使用)和一个外部名称(在调用函数时使用),参考外部参数名称。对方法参数也是一样的,因为方法仅仅是与某一类型相关的函数。但是,局部名称和外部名称的默认行为不同于函数和方法。

在 Swift 中的方法和在 Objective-C 中的方法极其相似,像在 Objective-C 一样,在 Swift 中方法的名称名称通常用一个介词指向方法的第一个参数,比如:withfor 以及 by 等等,前面的 Counter 类的例子中 incrementBy 方法就是这样的。当其被访问时,介词的使用使方法可被解读为一个句子介词的使用让方法在被调用时能像一个句子一样被解读。Swift 这种方法命名约定很容易落实,因为它是用不同的默认处理方法参数的方式,而不是用函数参数(来实现的)。

具体来说,Swift 默认仅给方法的第一个参数名称一个局部参数名称;但是默认同时给第二个和后续的参数名称局部参数名称和外部参数名称。这个约定与典型的命名和调用约定相匹配,这与你在写 Objective-C 的方法时很相似。这个约定还让 expressive method 调用不需要再检查/限定参数名。

看看下面这个 Counter 的替换版本(它定义了一个更复杂的 incrementBy 方法):

    class Counter {
        var count: Int = 0
        func incrementBy(amount: Int, numberOfTimes: Int) {
            count += amount * numberOfTimes
        }
    }

incrementBy 方法有两个参数:amountnumberOfTimes。默认地,Swift 仅把 amount 当做一个局部名称,但是把 numberOfTimes 既看作局部名称又看做外部名称。调用方法如下:

    let counter = Counter()
    counter.incrementBy(5, numberOfTimes: 3)
    // counter value is now 15

你不必对第一个参数值进行外部参数名称的定义,因为从函数名 incrementBy 已经能很清楚地看出它的目的/作用。但是,第二个参数,就要被一个外部参数名称所限定,以便在方法被调用时明确它的作用。

这种默认的行为能够有效的检查方法,比如你在参数 numberOfTimes 前写了个井号(#)时:

    func incrementBy(amount: Int, #numberOfTimes: Int) {
        count += amount * numberOfTimes
    }

这种默认行为使上面代码意味着:在 Swift 中定义方法使用了与 Objective-C 同样的语法风格,并且方法将以自然表达式的方式被调用。

修改方法的外部参数名称

有时,对一个方法的第一个参数提供一个外部参数名是有用的,即使这不是默认行为。你可以自己添加一个明确的外部名称或你也可以用一个 hash 符号作为第一个参数的前缀,然后用这个局部名字作为外部名字。

相反,若你不想为方法的第二或后续参数提供一个外部名称,你可以通过使用下划线 (_) 作为该参数的显式外部名称来覆盖默认行为。

self 属性

类型的每一实例都有一个被称为 self 的隐含属性,该属性完全等同于该实例本身。可以在一个实例的实例方法中使用这个隐含的 self 属性来引用当前实例。

在上面的例子中,increment 方法也可以被写成这样:

    func increment() {
        self.count++
    }

实际上,你不必在你的代码里面经常写 self。不论何时,在一个方法中使用一个已知的属性或者方法名称,如果你没有明确的写 self,Swift 假定你是指当前实例的属性或者方法。这种假定在上面的 Counter 中已经示范了:Counter 中的三个实例方法中都使用的是 count (而不是 self.count)。

这条规则的主要例外发生在当实例方法的某个参数名称与实例的某个属性名称相同时。在这种情况下,参数名称享有优先权,并且在引用属性时必须使用一种更恰当(被限定更严格)的方式。你可以使用隐藏的 self 属性来区分参数名称和属性名称。

下面的例子演示 self 消除方法参数x和实例属性 x 之间的歧义:

    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(1.0) {
        println("This point is to the right of the line where x == 1.0")
    prints "This point is to the right of the line where x == 1.0"

如果不使用 self 前缀,Swift 就认为两次使用的 x 都指的是名称为 x 的函数参数。

在实例方法中修改值类型

结构体和枚举均属于值类型一般情况下,值类型的属性不能在其实例方法内被修改。

但是,如果在某个具体方法中,你需要对结构体或枚举的属性进行修改,你可以选择变异(mutating)这个方法。方法可以从内部变异它的属性;并且它做的任何改变在方法结束时都会回写到原始结构。方法会给它隐含的 self 属性赋值一个全新的实例,这个新实例在方法结束后将替换原来的实例。

对于变异方法,将关键字 mutating 放到方法的 func 关键字之前就可以了:

    struct Point {
        var x = 0.0, y = 0.0
        mutating func moveByX(deltaX: Double, y deltaY: Double) {
            x += deltaX
            y += deltaY
        }
    }
    var somePoint = Point(x: 1.0, y: 1.0)
    somePoint.moveByX(2.0, y: 3.0)
    println("The point is now at (\(somePoint.x), \(somePoint.y))")
    // 输出 "The point is now at (3.0, 4.0)"

上文 Point 结构体定义一个变异(mutating)方法 moveByx,该方法按一定的数量移动 Point 结构体。moveByX 方法在被调用时修改了这个 point,而不是返回一个新的 point。方法定义是加上 mutating 关键字,因此,方法可以修改值类型的属性。

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

    let fixedPoint = Point(x: 3.0, y: 3.0)
    fixedPoint.moveByX(2.0, y: 3.0)
    // this will report an error

在变异方法中给 self 赋值

变异方法可以赋予隐含属性 self 一个全新的实例。上述 Point 的示例也可以采用下面的方式来改写:

    struct Point {
        var x = 0.0, y = 0.0
        mutating func moveByX(deltaX: Double, y deltaY: Double) {
            self = Point(x: x + deltaX, y: y + deltaY)
        }
    }

新版的变异方法 moveByX 创建了一个新的分支结构(他的 x 和 y 的值都被设定为目标值了)。调用这个版本的方法和调用上个版本的最终结果是一样的。

枚举的变异方法可以让 self 从相同的枚举设置为不同的成员:

    enum TriStateSwitch {
        case Off, Low, High
        mutating func next() {
            switch self {
            case Off:
                self = Low
            case Low:
                self = High
            case High:
                self = Off
        }
    ovenLight = TriStateSwitch.Low
    ovenLight.next()
    // ovenLight is now equal to .High
    ovenLight.next()
    // ovenLight is now equal to .Off

上述示例中定义了一个三态开关的枚举。每次调用 next 方法时,开关在不同的电源状态(Off, Low, High)之前循环切换。

类型方法

如上所述,实例方法是被类型的某个实例调用的方法。你也可以定义调用类型本身的方法。这种方法就叫做类型方法。声明类的类型方法,在方法的 func 关键字之前加上关键字 class;声明结构体和枚举的类型方法,在方法的 func 关键字之前加上关键字 static

注:在 Objective-C 中,你可以定义仅适用于 Objective-C 的类的 type-evel 方法。在 Swift,你可以对所有的类,结构体和枚举进行 type-evel 方法定义。每种类型方法仅适用于其所支持的类型。

与实例方法相同,也可利用点语法调用类型方法。但是,你需要在本类型上调用类型方法,而不是其类型实例上。下面是如何在 SomeClass 类上调用类型方法的示例:

    class SomeClass {
        class func someTypeMethod() {
        // type method implementation goes here
        }
    }
    SomeClass.someTypeMethod()

在一个类型方法的方法体内,self 指向该类型本身,而不是类型的某个实例。对结构体和枚举来讲,这意味着你可以用 self 来消除静态属性和静态方法参数之间的二意性(类似于我们在前面处理实例属性和实例方法参数时做的那样)。

更广泛地说,你在一个类型方法主体内使用的任何未经限定的方法和属性名称将指的是其他 type-level 方法和属性。一种类型方法能用其他方法名称调用另一种类型方法,而无需在方法名称前面加上类型名称的前缀。同样,结构体和枚举的类型方法也能够通过使用不带有类型名称前缀的静态属性名称来访问静态属性。

下述示例中定义名为 LevelTracker 的结构体,该结构体通过不同级别或阶段的游戏对玩家的进度进行监测。这是一个单人游戏,但也能在单个设备上储存多个玩家的信息。

所有游戏级别(除了级别 1)在游戏初始时都被锁定。每当玩家完成一个级别,在该设备上,该级别对所有的玩家解锁。LevelTracker 结构体用静态属性和方法来监测解锁的游戏级别。也可监测每个玩家的当前等级。

    struct LevelTracker {
        static var highestUnlockedLevel = 1
        static func unlockLevel(level: Int) {
            if level > highestUnlockedLevel 
                { highestUnlockedLevel = level }
        }
        static func levelIsUnlocked(level: Int) -> Bool {
            return level <= highestUnlockedLevel
        }
        var currentLevel = 1
        mutating func advanceToLevel(level: Int) -> Bool {
            if LevelTracker.levelIsUnlocked(level) {
                currentLevel = level
                return true
            } else {
                return false
        }
    }

LevelTracker 结构体监测任何玩家已解锁的最高级别。该值被存储在成为 highestUnlockedLevel 的静态属性中。

LeveTracker 还定义了两个类型函数与 highestUnlockedLevel 配合工作。第一个为 unockLevel 类型函数,一旦新的级别被解锁,该函数会更新 highestUnlockedLevel 的值。第二个为 levelIsUnocked 便利型函数,若某个给定级别数已经被解锁,该函数则返回 true。(注:没用使用 LevelTracker.highestUnlockedLevel,这个类型方法还是能够访问静态属性 highestUnlockedLevel。)

除了其静态属性和类型方法之外,LeveTracker 还监测每个玩家的游戏进程。它使用 currentLevel 实例属性来监测玩家当前进行的级别。

为便于管理 currentLevel 属性,LeveTracker 定义了实例方法 advanceToLevel。在更新 currentLevel 之前,该方法检查所要求的新级别是否已经解锁。advanceToLevel 方法返回布尔值以指示是否确实能够设置 currentLevel

下面,Player 类使用 LevelTracker 来监测和更新每个玩家的发展进度:

    class Player {
        var tracker = LevelTracker()
        let playerName: String
        func completedLevel(level: Int) {
            LevelTracker.unlockLevel(level + 1)
            tracker.advanceToLevel(level + 1)
        }
        init(name: String) {
            playerName = name

Player 类使用 LevelTracker 来监测该玩家的游戏进程。它也提供 competedLevel 方法,一旦玩家完成某个指定等级,调用该方法。该方法为所有玩家解锁下一个级别并将当前玩家进程更新为下一个级别。(忽略了 advanceToLevel 布尔返回值,因为之前调用上行时就知道了这个等级已经被解锁了。)

你可以为一个新玩家创建一个 Player 类的实例,然后看这个玩家完成等级一时发生了什么:

    var player = Player(name: "Argyrios")
    player.completedLevel(1)
    println("highest unlocked level is now \(LevelTracker.highestUnlockedLevel)")
    // prints "highest unlocked level is now 2"

如果你创建了第二个玩家,并尝试让他开始一个没有被任何玩家解锁的等级,关于设置玩家当前等级的尝试会失败:

    player = Player(name: "Beto")
    if player.tracker.advanceToLevel(6) {
        println("player is now on level 6")
    } else {
        println("level 6 has not yet been unlocked")
    }
    // prints "level 6 has not yet been unlocked"