学习Swift语言(三)属性方法和下标

312 阅读7分钟

一、属性

Swift 的属性有以下特点:

  • 类、结构体、枚举都可以定义属性;
  • Swift 的属性有两种:存储属性和计算属性。前者用于保存变量或常量值,而后者用于计算出某个值;
  • 存储属性和计算属性通常与特定类型的实例关联,也可以和类型本身关联;
  • 支持属性观察器。在类的定义代码中,可以给当前类的指定属性观察器,也可以给当前类的继承链上的类的属性指定观察器;
  • 可以使用 property wrapper 以在多个属性间复用 getter 和 setter 代码;

1.1 存储属性

存储属性是作为实例的一部分存储于类或结构体的实例中,可以是常量,也可以是变量。可以给属性指定默认值,也可以在构造实例时指定属性的默认值,对常量属性也不例外。

struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// the range represents integer values 0, 1, and 2
rangeOfThreeItems.firstValue = 6
// the range now represents integer values 6, 7, and 8

若构建结构体实例并赋值给一个常量后,就再不能通过该常量修改该结构体的任何属性值,包括该结构体的变量属性。这是因为结构体是值类型,将变量指定为常量实际上是将整个结构体实例,包括其属性在内,都指定为常量。但是在类中就不是如此,前面已经有举例介绍过。

注意:懒加载属性必须指定为var变量类型,因为实例初始化时很可能并没有计算该属性初始值。而 Swift 中常量属性在构造器执行完成前,必须指定初始值,所以let常量类型的属性不能指定为lazy

懒加载属性是指在正式使用该属性时才正式计算其初始值的属性。使用lazy关键字定义。懒加载属性的使用场景有两个:

  • 属性初始值依赖于外部因素,仅能在实例初始化完成后才能确定;
  • 属性初始值计算耗费资源或过程复杂,在实际需要使用时才有必要初始化;
class DataImporter {
    /*
    DataImporter is a class to import data from an external file.
    The class is assumed to take a nontrivial amount of time to initialize.
    */
    var filename = "data.txt"
    // the DataImporter class would provide data importing functionality here
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // the DataManager class would provide data management functionality here
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property has not yet been created

注意:若未初始化懒加载属性被多个线程同时访问,Swift 此时不能保证懒加载属性只被初始化一次。也就是说懒加载属性并不是线程安全的。

Objective-C 的类区分成员和属性,而 Swift 则将两者统一为属性。这有利于将属性定义代码集中于一处,包括属性名、类型、内存管理特征等等。同时避免了代码中对实例的关联值访问方式的不统一性。

1.2 计算属性

计算属性并不存储值,定义计算属性必须实现 getter 方法用对象的其他属性计算该计算属性的值,实现 setter 方法不是必须的,若不实现 setter 方法则表示该计算属性是只读属性。Setter 默认都需要传入一个该属性类型的参数,实现 setter 时可以缺省传入参数,缺省参数名为newValue

// 1. 以下是定义 center 计算属性的范例
struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
                  size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"

// 2. 简化 setter 的实现,使用缺省的 newValue 参数
struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

仅实现 getter 没实现 setter 的计算属性为只读属性。注意,声明只读属性的类型必须使用var,因为该属性值通常是根据其他属性值计算,并不是固定值。

struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// Prints "the volume of fourByFiveByTwo is 40.0"

1.3 属性观察器

属性观察器可以观察并响应属性值,当属性 setter 调用时,就会触发属性观察器方法,即便是 setter 传入参数就是当前属性值。

可以为类或继承类的存储属性指定观察器,但是不能为懒加载类添加观察器。计算属性不需要添加观察器,因为观察器逻辑可以直接添加到 setter 方法中。

观察器包含两种:

  • willSet:设置存储属性值之前自动触发;
  • didSet:设置存储属性值之后自动触发;
class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

注意:如果将对象的属性作为输入输出参数传入某个函数,则函数内部修改该输入输出参数值时,也会修改对象的属性值,当然也会触发对象该属性的观察器。

1.4 属性包装器

属性包装器(property wrapper)在属性的定义和实现之间添加一层抽象。可以将属性的实现代码声明为属性包装器,并在多个属性之间复用。定义属性包装器,需要通过结构体、枚举、类实现,用@propertyWrapper关键字声明。属性包装器需要包含以下几个要素:

  • @propertyWrapper标记;
  • 包装器主体;
  • 包装器包含存储属性;
  • 包装器包含构造器;
  • 包装器实现wrappedValue计算属性;
// 1. 基本属性包装器的定义
@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

// 2. 属性包装器的基本使用
struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

上面的使用属性包装器实现属性的等价代码如下,显著精简了代码。

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

声明属性包装器更像是修饰属性的原类型:

// 1. 声明属性包装器
@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

// 2. 用属性包装器声明属性1
struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"

// 3. 用属性包装器声明属性2
struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

// 4. 用属性包装器声明属性3
struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"

// 5. 用属性包装器声明属性4
struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1"

mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12"

除了wrappedValue,属性包装器还可以定义projectedValue属性,以支持更多的功能。属性包装器中定义wrappedValue是为了让类使用属性包装器通过.属性名访问属性值,而projectedValue是为了让类使用属性包装器通过.$属性名访问属性包装器的 projected value。Projected value 则是为了将属性包装器保存的值的特性暴露给类。

// 1. 用属性包装器定义 projected value
@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    init() {
        self.number = 0
        self.projectedValue = false
    }
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"

// 2. 使用属性包装器访问 projected value
enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

1.5 全局变量和局部变量

前面介绍的属性的计算类型和观察器特性同样适用于全局变量和局部变量。全局变量是在函数、方法、闭包或者类型上下文外部定义的变量,而局部变量则是在函数、方法、闭包或者类型上下文内部定义的变量。变量也支持计算变量、变量观察器的定义,语法和属性相近。

注意:全局常量和变量总是懒加载的。不同之处是全局变量是不需要lazy关键字来声明其懒加载特性的。因为缺省就是懒加载。但是局部变量则一定不是懒加载类型。

1.6 类型属性

前面介绍的属性都是与对象关联的,相当于 Objective-C 中的实例变量。而类型属性是与类型关联的,类似于 C++ 的类变量,而在 Objective-C 中没有对应的语法,通常使用静态变量实现类似的效果。Swift 使用static关键字声明类型属性,语法如下:

// 1. 定义类型属性
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

// 2. 访问类型属性
print(SomeStructure.storedTypeProperty)
// Prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Prints "Another value."
print(SomeEnumeration.computedTypeProperty)
// Prints "6"
print(SomeClass.computedTypeProperty)
// Prints "27"

二、方法

方法是与特定类型关联的函数。Swift 可以在结构体、枚举、类中定义方法,而且都支持实例方法和类型方法,所谓类型方法是和类型本身关联的方法,类似于 Objective-C 的类方法。在 Objective-C 中,方法的使用范围则仅限于类。

2.1 实例方法

实例方法语法如下,注意实例方法中使用属性,调用类的实例方法时可以缺省self

// 1. 类的实例方法定义
class Counter {
    var count = 0
    func increment() {
        count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}

// 2. 类的实例方法的使用
let counter = Counter()
// the initial counter value is 0
counter.increment()
// the counter's value is now 1
counter.increment(by: 5)
// the counter's value is now 6
counter.reset()
// the counter's value is now 0

结构体和枚举是值类型,因此其实例方法默认不能修改属性值,需要修改时必须用mutating关键字声明方法可以修改属性。需要注意在类的方法默认是可以修改属性值的,不需要声明mutating特性。

// 1. 值类型声明 mutating 方法
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))")
// Prints "The point is now at (3.0, 4.0)"

// 2. 值类型使用 mutating 方法
let fixedPoint = Point(x: 3.0, y: 3.0)
fixedPoint.moveBy(x: 2.0, y: 3.0)
// this will report an error

值类型的 mutating 方法可以给实例本身赋值

// 1. 声明赋值给结构体实例本身的方法
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)
    }
}

// 2. 声明赋值给枚举实例本身的方法
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 is now equal to .high
ovenLight.next()
// ovenLight is now equal to .off

2.2 类型方法

// 1. 声明类型方法
class SomeClass {
    class func someTypeMethod() {
        // type method implementation goes here
    }
}
SomeClass.someTypeMethod()

// 2. 声明值类型的 mutating 类型方法
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
        }
    }
}

// 3. 类的类型方法默认可以修改属性值,无需显式声明 mutating
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
    }
}

三、下标

Swift 的结构体、类、枚举都支持定义下标。用于访问实例中的集合、列表、序列中的元素,包括读取值和设置值。开发者可以给类型定义多个索引,Swift 可以根据索引的类型使用正确的下标方法。另外,Swift 定义下标不限定索引的个数。

3.1 下标语法

// 1. 下标的定义和使用
struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}
let threeTimesTable = TimesTable(multiplier: 3)
print("six times three is \(threeTimesTable[6])")
// Prints "six times three is 18"

3.2 多维下标

// 1. 定义多维下标
struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }
    func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

var matrix = Matrix(rows: 2, columns: 2)

// 2. 使用多维下标
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2

let someValue = matrix[2, 2]
// This triggers an assert, because [2, 2] is outside of the matrix bounds.

3.3 类型下标

可以定义与类型关联的下标,使用static定义类型下标。使用类型下标时,用类型触发。

// 1. 定义类型下标
enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
    static subscript(n: Int) -> Planet {
        return Planet(rawValue: n)!
    }
}

// 2. 使用类型下标
let mars = Planet[4]
print(mars)

四、总结

  • Swift 类型的属性分为存储属性和计算属性;
  • Swift 的属性支持属性观察器,包括willSetdidSet
  • Swift 的属性可以和类型的实例关联,也可以和类型本身关联,和类型关联的属性称为类型属性,相当于 C++ 类型中的类成员变量;
  • Swift 属性的可读写特性取决于 property wrapper 中包含的getset块,set块声明可使用缺省的newValue参数;
  • Swift 属性可以是常量也可以是变量,常量类型的属性值在完成初始化后就不可改变;
  • Swift 支持懒加载属性,在属性实际使用时才真正计算属性的值,注意懒加载属性必须是变量类型,懒加载属性并不是线程安全的;
  • Swift 的计算属性并不存储值;
  • Swift 支持属性包装器,属性包装器是将属性的初始化、getter、setter 逻辑集中到一个结构体中,该结构体可以保存属性的值,通过实现wrappedValue属性来指定属性包装器的数据存取逻辑;
  • Swift 定义属性包装器用@propertyWrapper、使用属性包装器用@${属性包装器名称}作为被包装的属性的类型;
  • Swift 属性包装器可以定义与属性相关联的属性,另外还可以关联 projected value,通过实现projectedValue属性来指定属性的 projected value,通常用于标记属性的状态;
  • Swift 属性的 getter、setter、属性观察期特性同样适用于全局变量和局部变量,Swift 的局部变量一定是懒加载的;
  • Swift 的类型属性需要用static修饰符修饰;
  • Swift 的结构体、枚举、类都支持方法的定义,Objective-C 中则仅有类才支持方法定义;
  • Swift 的方法包括实例方法和类型方法,后者需要用static修饰;
  • Swift 的结构体和枚举是值类型,其方法默认不可以修改实例,仅当声明为mutating时,才能修改实例;
  • Swift 的结构体、枚举、类都支持下标的定义;
  • Swift 的下标方法可定义多个,根据索引的类型区分,且不限定索引个数;
  • Swift 可以定义与类型相关联的下标,在枚举中用的比较多;