【Swift 3.1】10 - 屬性 (Properties)

屬性 (Properties)

自從蘋果2014年發布Swift,到現在已經兩年多了,而Swift也來到了3.1版本。去年利用工作之余,共花了兩個多月的時間把官方的Swift編程指南看完?,F在整理一下筆記,回顧一下以前的知識。有需要的同學可以去看官方文檔>>。


存儲屬性作為實例的一部分,存在常量和變量的值,而計算屬性是計算一個值,而不是存儲一個值。計算屬性由類、結構和枚舉提供;而存儲屬性只能由類和結構提供。

存儲和計算屬性通常有關聯著一個實例。然而,屬性可以關聯類型。

另外,可以定義屬性觀察者來監測屬性值的變化。屬性觀察者可以添加到自己定義的存儲屬性中,也可以添加到從父類繼承的屬性中。

存儲屬性 (Stored Properties)

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

firstValue是變量存儲屬性,length是常量存儲屬性。

常量結構實例的存儲屬性 (Stored Properties of Constant Structure Instances)

如果創建一個結構實例,并賦給一個常量,那么就不能修改實例的屬性,即使這個屬性是一個變量屬性:

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

因為rangeOfFourItems被聲明為常量,所以不能修改firstValue屬性,即使firstValue是變量屬性。

出現這種現象,其實是因為結構是值類型。只要實例是常量,那么它的屬性也都是常量。

而對于引用類型的類而言,把一個引用類型的實例賦值給一個常量,我們任然可以修改實例的變量屬性。

懶惰存儲屬性 (Lazy Stored Properties)

一個屬性在第一次使用之前,他的初始值沒有被計算,那么這個屬性稱為懶惰存儲屬性。

注意:懶惰存儲屬性必須聲明為一個變量,因為實例初始化完成后,懶惰屬性可能還沒被獲取。常量屬性在實例初始化完成之前必須有一個值,所有不能聲明為懶惰屬性。

當一個屬性的初始值依賴于外部因素,而外部因素的值需要在實例初始化完成之后才能確定,那么把這個屬性聲明為懶惰屬性非常有用。當一個屬性的初始值需要很復雜的計算時,懶惰屬性也非常有用。

下面這個例子使用懶惰屬性來避免不必要的一個復雜類的初始化。

class DataImporter {
    /*
     DataImporter is a class to import data from an external file.
     The class is assumed to take a non-trivial 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

DataManager定義了一個DataImporter實例,這里假設初始化DataImporter實例需要很長時間。DataManager實例可以自己管理數據,而不必使用DataImporter實例來打開文件,所以當創建DataManger實例時,無需把DataImporter實例同時創建,而是在第一次使用的時候才創建。

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

注意:如果一個懶惰屬性被多個線程同時訪問,而且這個屬性還沒有被初始化時,不能保證這個屬性只被初始化一次。

計算屬性 (Computed Properties)

計算屬性提供了一個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)"

這個例子定義了三個結構:

  • Point封裝了一個點的x和y坐標
  • Size封裝了寬和高
  • Rect使用原點和尺寸定義了一個矩形

Rect還定義了一個計算屬性center。矩形的中心可以同原點和尺寸計算出來,所以不必用存儲屬性來定義矩形的中心。Rect為計算屬性定義了一個自定義的getter和setter。

上面這個例子中創建了一個正方形square,如下圖中的藍色矩形。然后squarecenter屬性通過點語法被訪問,會使center的getter方法被調用,用來獲取屬性的當前值,getter會返回一個新的Point代表正方形的中心(當前是(5, 5))。

接著center被設置為(15, 15),把正方形往右上角移動,如下圖的橙色正方形。設置center屬性會調用setter方法,修改xy的值,把正方形移到一個新的位置。

Coordinate
簡寫Setter的定義 (Shorthand Setter Declaration)

如果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)
        }
    }
}
只讀計算屬性 (Read-Only Computed Properties)

一個計算屬性只有getter沒有setter,那么這個計算屬性就是只讀計算屬性。只讀計算屬性智能返回一個值,通過點語法訪問,但是不能設置新的值。

注意:必須用var把計算屬性(包括只讀計算屬性)定義為可變屬性,因為他們的值是不固定的。

定義只讀屬性時,我們可以把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"

屬性觀察者 (Property Observers)

屬性觀察者觀察屬性值得變化,并作出響應。只要屬性的值被設置,屬性觀察者就會被調用,即使新的值和屬性的當前值一樣。

我們可以把屬性觀察者添加到自己定義的任何存儲屬性中,除了懶惰存儲屬性。也可以通過重寫父類的屬性來添加到父類的屬性中。不必為沒有重寫的計算屬性定義屬性觀察者,因為我們可以在計算屬性的setter方法觀察他的值的變化,并作出響應。

兩種觀察屬性值變化的方法:

  • willSet:新的值被存儲之前調用
  • didSet:新的值被存儲之后馬上調用

如果實現一個willSet觀察者,會把新的值作為一個常量參數。可以為這個新的值指定一個名字,如果沒有指定,那么默認是newValue。

如果實現了一個didSet觀察者,會把舊的值作為一個常量參數??梢詾檫@個舊的值指定一個名字,如果沒有指定,那么默認是oldValue。如果在didSet觀察者里面給屬性賦一個新值,那么這個新值會替換掉剛剛設置的那個值。

注意:當父類的屬性在子類的初始化器被設置,那么父類屬性的willSetdidSet會在父類的初始化器調用之后被調用。在設置父類自己的屬性時,父類的willSetdidSet在父類的初始化器調用之前不會被調用。

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

注意:如果把含有觀察者的屬性作為一個in-out參數傳入一個方法,那么willSetdidSet總是會被調用,這是因為in-out參數的"copy-in copy-out"內存模式:在方法結束時,這個值會被返回被屬性。

全局和本地變量 (Global and Local Variables)

全局變量是在任何方法、閉包和類型上下文之外定義的。而局部變量是在方法、閉包和類型上下文之內定義的。

注意:全局常量和變量都是懶惰地計算,類似懶惰存儲屬性。但是全局常量和變量不需要使用lazy標記。本地常量和變量從來都不是懶惰的計算。

類型屬性 (Type Properties)

實例屬性是屬于一個特定類型的實例。我們可以定義屬于類型的屬性。

注意:不同于實例屬性,我們必須給類型屬性一個默認值。因為類型沒有初始化器來給類型屬性賦值。類型存儲屬性只要在第一次被訪問的時候才會初始化,并且保證只會初始化一次,即使是在多線程中同時被訪問,不需要使用lazy關鍵字標記。

類型屬性語法 (Type Property Syntax)

使用static關鍵字來定義類型屬性,對于class類型的計算類型屬性,可以使用class來定義,可以讓子類重新父類的實現。

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
    }
}
查詢和設置類型屬性 (Querying and Setting Type Properties)

就像實例屬性一樣,類型屬性也是通過點語法來查詢和設置的:

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 3.1】11 - 方法 (Functions)


如果有錯誤的地方,歡迎指正!謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容