類和結(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:setter
和willSet
共存?
我們知道,在同一個類中,屬性觀察和計算屬性是不能同時共存的,而且在計算屬性賦值時,你完全可以把想要做的檢查放在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)體和枚舉中定義的屬性都是實例屬性
。把實例屬性變成類型屬性
需要用static
或class
修飾。
注意:
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
。因為類作為引用類型,它修改屬性時無所謂實例本身是let
或var
,所以不需要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.