iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
Software Development

給我 30 天,給你一輩子:Swift 從零開始系列 第 21

Day 21 | Swift Class 與 Struct 快樂二選一:Struct 篇

  • 分享至 

  • xImage
  •  

Struct 結構

Struct 跟 Class 長得很像,連同使用方式都很像,這麼相似的兩個 Object Type,勢必會被拿來比較,但是這兩個到底什麼像,什麼不像,讓我們來看看。


Struct 宣告

先來看一下 Struct 的語法:

struct 結構名稱 {
	...
}

Struct 是使用 struct 關鍵字,後面接著結構名稱,至於結構名稱的命名與類別命名相同,一樣都是使用 Upper Camel Case

struct Dog {
	...
}

Struct 特性

Struct 跟類別的特性幾乎是一模一樣,一樣有屬性方法以及初始化

struct Dog {
    let id: Int
    var name: String
    var breed: String
    
    func run() {
        print("RUN!")
    }
}

咦?初始化呢?
為什麼沒跳出沒有初始化的錯誤訊息?

如果沒有定義初始值,不是應該透過 init() 執行建構式初始化?

沒有錯,在 Class 確實要這麼做,但是在 Struct 就不必這麼麻煩,因為會自動生成 initializer,像這種 Implicit Initializer 的稱作為 Memberwise Initializer

有了 Memberwise Initializer,他會自動幫我們把 Struct 中所有屬性都加進建構式中作為參數輸入,所以在實體化的時候,就可以直接使用這樣的建構式來建立實體:

let dog = Dog(id: 7533967, name: "皮皮", breed: "柯基")

但是你還是可以使用自訂的 initializer:

struct Dog {
    let id: Int
    var name: String
    var breed: String
    
    init(id: Int, name: String) {
        self.id = id
        self.name = name
        self.breed = "柯基"
    }
    
    func run() {
        print("RUN!")
    }
}

let dog = Dog(id: 7533967, name: "皮皮")

但是,當我們建立一個自定義的 Initializer 時,Memberwise Initializer 就不會被產生,自然就不能使用 Dog(id:name:breed:) 來建立實體。

Struct 不能繼承

在談談 Struct 不能繼承這件事之前,首先,我們先來了解什麼是繼承?

繼承( Inheritance ),是物件導向程式設計中其中一個特性,假使有一個 類別 A 繼承了 類別 B,這時候類別 A 具有本身的屬性、方法和初始值之外,也同時具有類別 B 的。

來舉一個簡單的例子,在《 Day 20 | Swift Class 與 Struct 快樂二選一:Class 篇 》中提到的 Car 類別,車子有很多種累,Super Car 也算是 Car 的一種,一樣都會有輪胎、車子烤漆及品牌;一樣可以發動引擎,在這種具有共通性的情況下,避免撰寫類似的程式碼,我們就可以透過繼承,來減少重複出現的程式碼。

class Car {
    var color: String
    var brand: String
    var wheelSize: Int
    
    init(color: String, brand: String, wheelSize: Int) {
        self.color = color
        self.brand = brand
        self.wheelSize = wheelSize
    }
    
    func engineStart() {
        print("拉風 引擎發動!")
    }
}

class SuperCar: Car {

}

SuperCar 繼承 Car,所以是 Car 的子類別,這時候就可以使用父類別的屬性、方法及建構式。

let superCar = SuperCar(color: "Verde Mantis", brand: "Lamborghini", wheelSize: 21)
print(superCar.color, superCar.brand, superCar.wheelSize)

superCar.engineStart()
// VerdeMantis Lamborghini 21
// 拉風 引擎發動!

但是 Super Car 也會有一些 Car 沒有的東西,還是可以在 Super Car 中,加入屬於自己類別的屬性或方法:

class SuperCar: Car {
    var mode: String
    
    init(color: String, brand: String, wheelSize: Int, mode: String) {
        self.mode = mode
        super.init(color: color, brand: brand, wheelSize: wheelSize)
    }
    
    func turnToMode() {
        switch self.mode {
        case let mode:
            print("Mode: \(mode)")
        }
    }
}
let superCar = SuperCar(color: "Verde Mantis", brand: "Lamborghini", wheelSize: 21, mode: "極速模式")

superCar.turnToMode()
// Mode: 極速模式

或是如果想要更改父類別的屬性或是方法,可以透過 override 的前綴字來複寫:

override func engineStart() {
    super.engineStart()
	  print("超跑引擎已發動")
}

透過 override 來複寫 CarengineStart() 方法,所以在調用上就會是 SuperCar 的 engineStart()

superCar.engineStart()
// 拉風 引擎發動!
// 超跑引擎已發動

所以繼承能夠避免重複撰寫相似的程式碼,並且在同類型的類別中,又能在客製自己獨特的屬性、方法或是 Initializer。

扯這麼多,Struct 就是不能做到這件事情,但也不是完全不能繼承哪個有錢的老爸,還是有方法可以做到類似繼承,並且比繼承還好,先提示一下,那個大招叫做 Protocol,更多細節請看接下來的幾個章節 ><

Value Type 以及 Reference Type

咦?怎麼突然提到這個?

Class 跟 Struct 最大的差別就在於:

  • Class 是 Reference Type
  • Struct 是 Value Type

那什麼是 Reference Type?Value Type 又是哪位?

Value Type

直接來看範例最清楚:

struct People {
    var name: String
}

var people1 = People(name: "張三")
var people2 = people1
print("1: \(people1.name), 2: \(people2.name)")
// 1: 張三, 2: 張三

people1.name = "李四"
print("1: \(people1.name), 2: \(people2.name)")
// 1: 李四, 2: 張三

建立了一個 People 的實體 people1,再指派給 people2,這時候兩個的 name 相同,但如果更改了 people1name,但是 people2 卻還是沒變,這就是 Value Type

雖然 people2 是 people1 指派過去的,將內容複製一份過去,但兩者則是分別佔據不同的記憶體空間,所以不會互相影響。

withUnsafeMutablePointer(to: &people1) {
    print($0) // 0x0000000109c92750
}

withUnsafeMutablePointer(to: &people2) {
    print($0) // 0x0000000109c92760
}

Reference Type

反之 Reference Type 則會共享同一個記憶體位址:

class People {
    var name = "張三"
}

let people1 = People()
let people2 = people1

print("1: \(people1.name), 2: \(people2.name)")
// 1: 張三, 2: 張三

people1.name = "李四"
print("1: \(people1.name), 2: \(people2.name)")
// 1: 李四, 2: 李四
print(Unmanaged.passUnretained(people1).toOpaque())
// 0x0000600003522a80

print(Unmanaged.passUnretained(people2).toOpaque())
// 0x0000600003522a80

也因為共享同一個記憶體位址,所以當一個物件屬性變動時,另一個也會跟著變動。

至於 Struct 與 Class 的抉擇,Apple 在官方文件上有提到了四點:

  • Use structures by default.
  • Use classes when you need Objective-C interoperability.
  • Use classes when you need to control the identity of the data you're modeling.
  • Use structures along with protocols to adopt behavior by sharing implementations.

在以往 OOP 的觀念下,通常都會優先使用 Class 作為資料存取的優先考量,但是 Apple 持續地推廣 協定導向設計模式 (POP, Protocol Oriented Programming) 的觀念,有別繼承這個特性,有了新的觀點,也使得 Struct 有了更多合理的使用理由,至於 OOP 與 POP 的差異,會在接下來詳細討論。


上一篇
Day 20 | Swift Class 與 Struct 快樂二選一:Class 篇
下一篇
Day 22 | Swift Property 一家親:Stored Property 和 Computed Property
系列文
給我 30 天,給你一輩子:Swift 從零開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言