Swift文档翻译计划 -- 属性

423 阅读10分钟

目录

  • 存储属性:将常量和变量值存储为实例的一部分,仅由类和结构体提供。
  • 计算属性:计算值非存储,计算属性由类、结构体和枚举提供。
  • 类型属性:与类型本身相关联的属性。
  • 属性观察者:监视属性值的更改,属性观察者可以添加到自定义的存储属性中,也可以添加到子类从超类继承的属性中。还可以使用属性包装器重用多个属性的 getter 和 setter 中的代码。

存储属性

常量结构体实例的存储属性

如果一个结构体的实例赋值给了一个常量,则不能修改实例的属性,即使属性被声明为了变量:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// this range represents integer values 0, 1, 2, and 3
rangeOfFourItems.firstValue = 6
// this will report an error, even though firstValue is a variable property

这是因为结构体是值类型。当值类型的实例被标记为常量时,它的所有属性也被标记为常量。对于引用类型的类则不是这样。如果将引用类型的实例分配给常量,仍然可以更改该实例的变量属性。

惰性存储属性

延迟存储属性的初始值直到第一次使用时才计算。在属性定义之前通过声明lazy 修饰符来标识这是一个惰性存储属性。

⚠️惰性存储属性必须使用 var 声明为可变类型,因为在实例初始化完成后可能不会检查到它的初始值。常量属性在初始化完成之前必须始终具有一个值,因此不能声明为 lazy。

什么时候使用 lazy 声明属性?当属性的初始值需要进行复杂或计算开销较大的设置,直到需要才执行的时候 lazy 会非常有用,类似于编写 OC 时的懒加载,使用的时候才会被初始化。如下面例子所示:

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

importer 属性使用 lazy 关键字,表示懒加载,只有在访问的时候加载,因为它有可能是个耗时操作没有必要在 DataManager 初始化时就初始化 DataImporter。例如当它的filename属性被查询时:

print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"

如果标记为 lazy 修饰符的属性被多个线程同时访问,并且该属性尚未初始化,则不能保证该属性将只初始化一次。

计算属性

计算属性不存储值。相反,会提供一个 getter 和一个可选的 setter 来间接地检索和设置其他属性和值。

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)"

Rect 结构体提供了一个名为 center 的计算属性。只要知道 Rect 的原点和大小,就能确定中心位置,因此不需要将中心点存储为显式的点值。相反,Rect 为名为 center 的计算变量定义了自定义 getter 和 setter,能够像处理存储的属性一样处理矩形的中心。

上面的示例创建了一个名为 square 的新 Rect 变量。square 变量的初始值为 (0,0),宽度和高度为 10。这个正方形由下图中的蓝色正方形表示。

然后通过点语法 square.center 访问 square 变量的 center 属性,这会导致调用 center 的 getter 来检索当前属性值。getter 并不返回现有的值,而是通过计算并返回一个新点来表示正方形的中心。从上面可以看到,getter 正确地返回一个中心点 (5,5)。

然后,center 属性被设置为一个新值 (15,15),该值将方块向上和向右移动到图中橙色方块所示的新位置。设置 center 属性将调用 center 的 setter,该 setter 修改存储的 origin 属性的 x 和 y 值,并将正方形移动到它的新位置。

速记Setter宣言

如果计算属性的 setter 没有为要设置的新值定义名称,则使用默认名称 newValue。下面是一个利用了这种速记符号的替代版本 Rect 结构体:

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宣言

如果 getter 的整个主体是一个表达式,则 getter 会隐式返回该表达式。这里是另一个版本的Rect 结构,利用了这种速记符号和 setter 的速记符号:

struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                  y: origin.y + (size.height / 2))
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Read-Only计算属性

带有 getter 但没有 setter 的计算属性称为只读计算属性。只读计算属性总是返回一个值,并且可以通过点语法访问,但是不能设置新值。

必须使用var关键字将计算属性(包括只读计算属性)声明为变量属性,因为它们的值不是固定的。let关键字仅用于常量属性,以指示一旦将其值设置为实例初始化的一部分,就不能更改它们。

可以删除 get 关键字及其大括号简化只读计算属性的声明:

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"

理论上这个例子的体积是由长宽高决定的,所以将其设置为只读是合理的。

属性观察器

类似于 OC 中的 kvo,属性改变监听属性的新值和旧值。属性观察者观察并响应属性值的变化。每当设置属性的值时,都会调用属性观察器,即使新值与属性的当前值相同。

属性观察器可以添加在以下位置:

  • 定义存储属性
  • 继承的存储属性
  • 继承的计算属性

对于计算属性,使用属性的 setter 来观察和响应值的更改,而不是尝试创建一个观察器。

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

使用 willSet 和 didSet 监听值的变化。

  • willSet 在存储值之前被调用。
  • didSet 在新值存储后立即被调用。

当在子类初始化器中设置父类的属性时,在调用父类初始化器之后,父类属性的 willSet 和 didSet 观察者将被调用。子类设置自己的属性时,在父类初始化器被调用之前,它们不会被调用。

属性包装器

属性包装器在存储属性的代码和定义属性的代码之间添加一个中间层,在使用属性的时候这个中间层会处理一些逻辑,例如线程安全检查等等。

定义

包装属性需要在结构体、类或者枚举中定义 wrappedValue 属性,例如下面的例子,定义一个 TwelveOrLess 结构体,用于对外部返回一个始终小于或等于 12 的值:

@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    init() { self.number = 0 }
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

number 使用 private 声明确保外部访问 number 只能通过 wrappedValue,不能直接使用 number。

使用

通过将包装器的名称作为属性写入属性前面,可以将包装器应用到属性。例如结构体 SmallRectangle,它用来存储矩形的宽高:

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"

尽管给 height 设置了 24,但打印的结果仍然是 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 }
    }
}

_height_width 属性存储了属性包装器的实例 TwelveOrLess。用于高度和宽度的 getter 和 setter 改变了对 wrappedValue 属性的访问。

设置初始值

上面的例子 number 的初始值被定义在了 TwelveOrLess 结构体中,不能由外部指定初始值,SmallRectangle 也不能定义宽高的初始值,下面是 TwelveOrLess 的扩展版本 SmallNumber 结构体的定义,它提供了三个初始化方法:

@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)
    }
}

当不指定初始值时,Swift 默认使用 init() 初始化器来设置包装器。例如:

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

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

当为属性指定初始值时,Swift 使用 init(wrappedValue:) 初始化器来设置包装器。例如:

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

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

当在自定义属性后的圆括号中编写参数时,Swift 使用接受这些参数的初始化器来设置包装器:

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"

当包装器包含参数且对属性指定了初始值。Swift 将赋值认为是处理 wrappedValue 参数,并使用接受所包含参数的初始化器。例如:

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"

封装高度的 SmallNumber 实例是通过调用 SmallNumber(wrappedValue: 1) 创建的,它使用默认的最大值12。封装宽度的实例是通过调用 SmallNumber(wrappedValue: 2, maximum: 9) 创建的。

从属性包装器映射值

除了包装值之外,属性包装器还可以通过定义投影值公开其他功能。如下面的代码将一个 projectedValue 属性添加到 SmallNumber 结构体中,以跟踪属性包装器在存储新值之前是否调整了该属性的新值。

@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"

在本例中,属性包装器将布尔值作为投影值公开,表示数字是否被调整过。其实属性包装器可以返回任何类型的值作为其投影值。需要公开更多信息的话,包装器可以直接返回某个其他数据类型的实例,或者它可以返回 self 以将包装器的实例作为投射值公开。

当从属于该类型的代码(如属性getter或实例方法)访问投影值时,可以省略self。在属性名之前,就像访问其他属性一样。下面示例中的代码,包装器将高度和宽度的投影值作为 $height$width:

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
    }
}

如果调用了 resize(to: .large),将矩形的高度和宽度设置为 100,包装器防止这些属性的值大于12,并将投射值设置为true,用以记录值被调整了,在 resize(to:) 的末尾,return 语句检查 $height$width,以确定属性包装器是否调整了高度或宽度。