類和結(jié)構(gòu)體的小總結(jié)

類和結(jié)構(gòu)體是兩種很重要的數(shù)據(jù)結(jié)構(gòu),在Swift中類和結(jié)構(gòu)體無論是從語法還是用法上都高度相似,但是功能和原理又大相徑庭。

在Swift中,類和結(jié)構(gòu)體的主要區(qū)別:
1 結(jié)構(gòu)體不支持繼承和類型轉(zhuǎn)換。
2 結(jié)構(gòu)體不支持定義析構(gòu)方法。
3 結(jié)構(gòu)體是值類型,類是引用類型。

由于他們分別是值類型和引用類型,導致他們在內(nèi)存分布引用計數(shù)、方法派發(fā)等方面都有明顯差異。
當然,他們也有很多共同點,比如屬性、方法構(gòu)造、下標擴展、實現(xiàn)協(xié)議等方面。

相似點

1 屬性

這里涉及的知識點有存儲屬性、計算屬性、延遲存儲屬性屬性觀察者、閉包初始化等。

1.1 存儲屬性

即我們平時常見的變量(var)或常量(let),可以在聲明時指定默認值,也可以在構(gòu)造過程中指定。
注意:
1 不能在枚舉中使用
2 不能被子類重寫,但是可以在子類中進行屬性觀察

// 常量存儲屬性
let id: Int
// 變量存儲屬性
var firstValue: Int
// 延遲存儲屬性(懶加載)
lazy var 存儲屬性名 : 類型 = { 創(chuàng)建變量代碼 }()

值得注意的是,即便是聲明為常量的存儲屬性,在構(gòu)造階段依然可以修改。但是這個常量必須不能有默認值,因為Swift中常量屬性只能被初始化一次。

class Question {
    let text: String // 如果text屬性預(yù)先指定了初始值,則編譯不通過
    init(text: String) {
        self.text = text
    }
}
1.2 計算屬性

計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設(shè)置其他屬性或變量的值。
注意:
1 不能用let修飾計算屬性

var 名稱: 類型 {
    get { return }
    set(newValue) { }
}

這里setter方法的newValue可以省略,且setter方法中依然可以使用省略的newValue值。甚至可以直接省略setter方法,將屬名聲明為只讀計算屬性。

// 只讀存儲屬性
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)
1.3 屬性觀察者

為了讓屬性被賦值時獲得執(zhí)行代碼的機會,在Swift中,屬性觀察者其實就是兩個特殊的回調(diào)方法。
注意:
1 不要為沒有重載的、普通的計算屬性添加屬性觀察者(本身對應(yīng)set部分已經(jīng)獲得執(zhí)行)
2 不要為常量存儲屬性、只讀計算屬性添加屬性觀察者(它們不會發(fā)生改變)

// 語法格式
var 存儲屬性名 : 類型 = 初始值 {
  willSet(newValue){ 即將賦值前 }
  didSet(oldValue){ 賦值完成后 }
}
// newValue和oldValue這兩個參數(shù)都可以省略

Q:setterwillSet共存?
我們知道,在同一個類中,屬性觀察和計算屬性是不能同時共存的,而且在計算屬性賦值時,你完全可以把想要做的檢查放在setter方法中。所以不存在setter和willSet共存。但是假如我們不能修改setter方法中的代碼,又想要做一些事情,我們可以通過繼承并對父類的計算屬性進行觀察。

class A {
    var number: Int {
        get {
            print("get")
            return 0
        }
        set { print("set") }
    }
}

class B: A {
    override var number: Int {
        willSet { print("willSet") }
        didSet { print("didSet") }
    }
}
1.4 閉包初始化

在定義存儲屬性之后,后面緊跟一個閉包進行初始化,這個閉包會在類初始化時直接調(diào)用。
注意:區(qū)分閉包初始化和懶加載的寫法

let purpleView: UIView = {
    // 在此初始化 view
    // 直接叫 "view" 真的好嗎?
    let view = UIView()
    view.backgroundColor = UIColor.purple
    return view
}()

// 閉包初始化時面臨一個問題:需要給中間變量進行命名
// 這種寫法可以省略中間變量
let purpleView: UIView = {
    $0.backgroundColor = UIColor.purple
    return $0
}(UIView())
1.5 類型屬性(靜態(tài)屬性)

在不加特定修飾的情況下,Swift在類、結(jié)構(gòu)體和枚舉中定義的屬性都是實例屬性。把實例屬性變成類型屬性需要用staticclass修飾。
注意:
1 類型屬性必須在聲明時就添加默認值或聲明為可選類型
2 類中計算屬性可以用class或static修飾,存儲屬性只能用static修飾
3 結(jié)構(gòu)體不論計算屬性還是存儲屬性都用static修飾

struct SomeStructure {
    // 結(jié)構(gòu)體中都用static修飾
    static var storedTypeProperty = "Some value"
    static var computedTypeProperty: Int {
      return 0
    }
}
class SomeClass {
    static var storedTypeProperty = "Some value" // 存儲屬性用static修飾
    class var computedTypeProperty: Int { // 計算屬性用class修飾
      return 0
    }
}

2 方法

在Swift中函數(shù)和方法語法格式相同,定義在枚舉、類和結(jié)構(gòu)體內(nèi)部的稱之為方法。外部的就是函數(shù)。結(jié)構(gòu)體和枚舉能夠定義方法是 Swift 與Objective-C 的主要區(qū)別之一。方法同時也有實例方法類型方法之分。

2.1 mutating關(guān)鍵字

默認情況下,結(jié)構(gòu)體中實例方法內(nèi)是不可以修改其屬性值(因為結(jié)構(gòu)體是值類型)。類中的實例方法中可以修改其屬性值。
所以結(jié)構(gòu)體中要用mutating關(guān)鍵字來修飾實例方法,才能在方法中修改屬性值。

// 結(jié)構(gòu)體
struct Point {
    var x = 0, y = 0
    mutating func moveByPoint(x:Int, y:Int) {
        self.x += x
        self.y += y
    }
}
var point = Point(x: 3, y: 3) // 這里要用var修飾,mutating關(guān)鍵字不能用在常量上

// 枚舉
enum BlackOrWhite {
    case Black, White
    mutating func switchColor() {
        switch self {
        case .Black:
            self = .White
        case .White:
            self = .Black
        }
    }
}

Swift 的協(xié)議不僅可以被類實現(xiàn),也適用于結(jié)構(gòu)體和枚舉類型。因此,我們在寫給別人用的接口時需要多考慮是否使用mutating來修飾方法。

// 協(xié)議
protocol Vehicle {
    var numberOfWheels: Int {get}
    var color: UIColor {get set}
    mutating func changeColor(to color:UIColor)
}

class MyCar: Vehicle {
    let numberOfWheels = 4
    var color = UIColor.blue
    // class中實現(xiàn)帶mutating方法的接口時,不用進行mutating修飾
    func changeColor(to color: UIColor) { 
        self.color = color
    }
}

struct YourCar: Vehicle {
    let numberOfWheels = 6
    var color = UIColor.yellow
    mutating func changeColor(to color: UIColor) {
        self.color = color
    }
}

思考:為什么結(jié)構(gòu)體的實例方法中不能直接修改屬性值呢?
我猜測是因為結(jié)構(gòu)體在初始化階段內(nèi)部的屬性和方法等所占用的內(nèi)存空間已經(jīng)分配完成。而且結(jié)構(gòu)體本身值類型分配在沒那么靈活的棧中,mutating關(guān)鍵字修飾的方法會在被實例調(diào)用時強制要求該實例聲明為var。因為類作為引用類型,它修改屬性時無所謂實例本身是letvar,所以不需要mutating修飾。

思考:下面這個結(jié)構(gòu)體,調(diào)用change()方法,結(jié)構(gòu)體實例的地址是否改變?

struct CoordinateStruct {
    var x: Double
    var y: Double
    mutating func change() {
        self = CoordinateStruct(x: 10, y: 20)
    }
}
2.2 ===、!==

在Swift中,引用類型可以直接通過===!==比較兩個引用類型的變量是否指向同一個對象。

class Dog {
    var height = 0.0
    var weight = 0.0
}

var dogA = Dog()
var dogB = dogA
print(dogA===dogB) // true

然而,如果直接使用==!=比較的話會導致編譯錯誤。但是我們可以通過實現(xiàn)Equatable協(xié)議來進行運算符重載。

extension Dog: Equatable {
    static func == (lhs: Dog, rhs: Dog) -> Bool {
        return lhs.height == rhs.height && lhs.weight == rhs.weight
    }
    static func != (lhs: Dog, rhs: Dog) -> Bool {
        return lhs.height != rhs.height || lhs.weight != rhs.weight
    }
}

值得注意的是,Swift中運算符本質(zhì)上都是運算符重載,值類型也同樣可以重載它們。例如:===(left: Any, right:Any)->Bool,其中Any既可以代表值類型,又可以代表引用類型。

2.2 類型方法

在 Objective-C 里面,你只能為 Objective-C 的類定義類型方法。在 Swift 中,你可以為所有的類、結(jié)構(gòu)體和枚舉定義類型方法:每一個類型方法都被它所支持的類型顯式包含。
注意:
1 結(jié)構(gòu)體、枚舉的類型方法使用static修飾
2 類的類型方法可以用staitc或class修飾,使用class修飾的可以被子類重寫

3 構(gòu)造

前面簡單介紹了屬性和方法,他們都是在定義結(jié)構(gòu)體或類時重要的部分,那么創(chuàng)建實例就一定要講到構(gòu)造方法。
文檔中涉及的知識點比較多,我把我認為重要的拿出來對比一下,分別是默認構(gòu)造器、便捷構(gòu)造器、可能失敗的構(gòu)造器等。

3.1 默認構(gòu)造器

對于類而言:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

情況1:只有當該類中所有實例存儲屬性顯式指定了初始值,或者聲明為可選類型,且沒有提供任何構(gòu)造器時,系統(tǒng)才會為該類提供一個無參數(shù)的構(gòu)造器。即ShoppingListItem()。

class ShoppingListItem {
    var name: String // 存儲屬性name沒有初始值
    var quantity = 1
    var purchased = false
    init(name: String) {
        self.name = name
    }
}
var item = ShoppingListItem(name: "water") // 原本的空構(gòu)造器失效

情況2: 如果提供了一個構(gòu)造器,可以把沒有指定初始值的屬性在構(gòu)造器中進行初始化,不過此時系統(tǒng)默認的空構(gòu)造器就不能使用了,只能使用自己定義的構(gòu)造器,即ShoppingListItem(name: "water")。

對于結(jié)構(gòu)體而言:

struct Size {
    var width = 1.0, height = 1.0
    var area: Double {
        return width * height
    }
}
let twoByTwo = Size(width: 2.0, height: 2.0)
let defaultSize = Size()

注意:結(jié)構(gòu)體中不一定要給所有的實例存儲屬性初始值指定初始值,或者聲明為可選,因為結(jié)構(gòu)體會默認提供一個初始化所有屬性的構(gòu)造器。即Size(width: Double, height: Double)
如果給所有屬性指定了初始值,除了包含所有屬性的構(gòu)造器以外,系統(tǒng)還會提供一個空的構(gòu)造器。即Size()。

總結(jié):無論是類還是結(jié)構(gòu)體
1 沒有給所有存儲屬性初始值時,類會報錯,結(jié)構(gòu)體會提供一個所有屬性的構(gòu)造器。
2 給所有存儲屬性初始值時,類會提供一個空構(gòu)造器,結(jié)構(gòu)體會提供兩個構(gòu)造器(一個空,一個所有參數(shù))。
3 如果他們自己實現(xiàn)了構(gòu)造器,那么只能用自己的構(gòu)造器(前提是所有屬性都初始化了)。

這里補充一下關(guān)于OC和Swift中構(gòu)造器的不同:
1 Swift的構(gòu)造器名總為init方法(無需func修飾),且沒有聲明返回值類型,也無需return語句,系統(tǒng)隱式地返回self作為返回值。
2 OC的構(gòu)造器必須聲明返回值類型(id或instanceType),由于沒有“重載”的概念,所以需要通過參數(shù)列表區(qū)分不同的構(gòu)造方法。

3.2 便捷構(gòu)造器和可失敗的構(gòu)造器

首先便捷構(gòu)造器就是一種便捷的寫法,常用于嵌套自定義的構(gòu)造器給定一些初始值,聲明便捷構(gòu)造器需要使用convenience關(guān)鍵字。

class Cat {
    let name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "hotdog")
    }
}
let cat = Cat()

其次可失敗的構(gòu)造器,無論是類,結(jié)構(gòu)體或枚舉類型的對象,在構(gòu)造自身的過程中有可能失敗,則為其定義一個可失敗構(gòu)造器,是非常有必要的。這里所指的“失敗”是指,如給構(gòu)造器傳入無效的參數(shù)值,或缺少某種所需的外部資源,又或是不滿足某種必要的條件等。

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}
let animal = Animal(species: "")

思考:下面是Person類的代碼,構(gòu)造person實例時會輸出什么結(jié)果?

class Person {
    var age: Int {
        print("只讀計算屬性")
        return 0
    }
    lazy var firstName: String = {
        print("懶加載")
        return ""
    }()
    var nickName: String = {
        print("閉包初始化")
        return ""
    }()
    var info: String = "" {
        didSet{
            print("屬性觀察" + oldValue)
        }
        willSet {
            print("屬性觀察" + newValue)
        }
    }
}
let person = Person() 
// 閉包初始化

很明顯,實例化person時會打印“閉包初始化”,這個例子包含了之前屬性的一些知識點,有可能會在筆試環(huán)節(jié)出現(xiàn)。

不同點

與其說類和結(jié)構(gòu)體的不同點,不如放大命題為值類型和引用類型的不同點。

Swift 中幾乎所有的內(nèi)建類型都是值類型,不僅包括了傳統(tǒng)意義像 Int,Bool 這些,甚至連 String,Array 以及 Dictionary 都是值類型的(與OC中不同,在OC中Int,Bool是基本數(shù)據(jù)類型,而NSString,NSArray等都派生自NSObject)。

對于值類型和引用類型的區(qū)別,網(wǎng)絡(luò)上存在大量的理論論述的文章,很多都寫得很深刻也很全面。下面給大家推薦幾篇:

這篇文章就不贅述原理了,而是從結(jié)論直接出發(fā),擴充一些常見的問題加以支撐。


題目整理(不斷更新)

1 swift中Array是什么類型,大量復制時會不會影響性能?
首先,我們都知道在Swift中的Array是值類型,值類型是存儲在棧中,每個實例保持一份數(shù)據(jù)拷貝,也就是說在一般情況下,值類型的賦值是深拷貝。那么大量復制必然會影響性能才對?這顯然不合常理,所以帶著疑問看下面的代碼。

func address<T: AnyObject>(of object: T) -> String {
    let addr = unsafeBitCast(object, to: Int.self)
    return String(format: "%p", addr)
}

func address(of object: UnsafeRawPointer) -> String {
    let addr = Int(bitPattern: object)
    return String(format: "%p", addr)
}

定義兩個查看變量內(nèi)存地址的方法,參考Printing a variable memory address in swift,并做修改。

var arr = [1,2,3,4]
let newArr = arr
print(address(of: arr)) // 0x600000a18ca0
print(address(of: newArr)) // 0x600000a18ca0

可以看出,在數(shù)組進行簡單賦值時,兩個變量指向的內(nèi)存地址是相同的。那么它什么時候才拷貝呢?

arr.append(5)
print(address(of: arr)) // 0x6000023f9ac0
print(address(of: newArr)) // 0x600001290760

我們發(fā)現(xiàn),對于原數(shù)組進行修改操作之后,他們指向了不同的內(nèi)存地址,說明此時數(shù)組進行了拷貝。其實對于值類型有這樣一條結(jié)論:基本數(shù)據(jù)類型在賦值的時候復制,集合類型(Array, Set, Dictionary)是在寫時復制的。這是Swift對結(jié)構(gòu)體類型的一種優(yōu)化,所以在Swift中大量復制Array時是不會影響性能的,因為只是做了指針移動。

2 模型拷貝
在實際開發(fā)中,經(jīng)常需要把列表中的數(shù)據(jù)解析成模型封裝在數(shù)組中。而Model大多定義為Class類型,那么在數(shù)據(jù)傳遞時,經(jīng)常發(fā)現(xiàn)列表中的數(shù)據(jù)莫名其妙被修改了,這是什么原因呢?

我們模擬一下上述的場景:

class Person {
    var name = ""
    // ... 
}
// 創(chuàng)建一個person對象,并封裝在數(shù)組中。
let person = Person()
let arr = [person]
let newArr = arr
print(address(of: arr)) // 0x600000305220
print(address(of: newArr)) // 0x600000305220

截止到這里,我們都很熟悉,因為與上一個問題情況相同,數(shù)組沒有發(fā)生寫入操作時都指向相同的內(nèi)存地址,不同的是上一個問題數(shù)組中封裝的是值類型,而這次數(shù)組封裝的是引用類型。這就涉及了類型嵌套,與上一題的內(nèi)存分布形式不同。

person.name = "Rikcy"
print(arr[0].name) // Rikcy
print(newArr[0].name) // Rikcy

由于兩個數(shù)組中的person對象指向同一個堆中的地址,所以他們數(shù)組中person.name都被修改了。注意:此時他們的內(nèi)存地址依然相同,因為數(shù)組本身沒有發(fā)生寫入操作。

那么對于上述的問題,同樣有兩個解決方案:1 封裝Model時盡量選擇Struct類型(避免在結(jié)構(gòu)體中嵌套額外的引用類型)2 對于Class類型的Model進行拷貝。

介紹一下第二種解決方案:定義一個Copying協(xié)議,同時實現(xiàn)對應(yīng)的方法。

// 定義Copying協(xié)議
protocol Copying {
    init(original: Self)
}
extension Copying {
    func copy() -> Self {
        return Self.init(original: self)
    }
}
// 遵循Copying協(xié)議,并實現(xiàn)copy()方法
class Person: Copying {
    var name = ""
    required init(original: Person) {
        self.name = original.name
    }
}

// 在數(shù)組賦值時,循環(huán)調(diào)用copy方法。
var persons = [person]
var newpersons = [Person]()
for item in persons {
    newpersons.append(item.copy())
}

這里的寫法就很多了,你也可以運用之前講到的mutating關(guān)鍵字,同樣是聲明的協(xié)議中,簡化的寫法是在Model的基類中實現(xiàn)該協(xié)議,利用Runtime遍歷ivarsList并賦值等。

3 ===、!==
關(guān)于引用類型的比較,上面涉及過。

4 PS:關(guān)于函數(shù)In-Out關(guān)鍵字
關(guān)于值類型或者引用類型作為函數(shù)參數(shù)傳遞的相關(guān)知識點,之前寫過一篇文章,并在最近重讀更新了一次。
有興趣的可以看看:函數(shù)In-Out關(guān)鍵字

不斷更新ing.

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

推薦閱讀更多精彩內(nèi)容