今天的題目,在 iOS 開發是必須要知道的,討論度也很高,但由於本人目前的開發經驗還處於新手階段,對於如何選擇結構 (Struct) 和類 (Class) 仍不是相當熟悉,所以在本文後段有放入蘋果官網提供的結構與類的選擇建議。
結構和類是通用且靈活的結構,它們可用來建構程式代碼區塊。我們可以使用與定義常量,變量和函數相同的語法來定義屬性和方法,以便為結構和類添加功能。
與其他程式語言不同,Swift 沒有要求我們為自訂的結構和類建立單獨的介面和實現的文件。在 Swift 中,我們定義結構和類在單一的文件,而類或結構的外部介面會自動產生給其他的程式使用。
傳統上將類的實例稱為物件 (object)。但是,Swift 結構和類在功能上比其他語言要緊密得多,本章的大部分內容描述了適用於類或結構型別的實例的功能。因此,使用更通用的術語實例 (instance)。
在 Swift 中的結構與類有許多相同之處,兩者皆能:
類具有的附加功能,但結構沒有:
類支持的附加功能是以增加複雜性為代價的。作為一般準則,應首選結構,因為它們更易於推斷,並在適當或必要時使用類。實際上,這代表著定義的大多數自定義數據型別將是結構和列舉。
結構跟類擁有類似的定義語法,結構的關鍵字為 struct
,而類的關鍵字為 class
。兩者都將其整個定義放置在一對大括弧中:
struct SomeStructure {
// structure definition goes here
}
class SomeClass {
// class definition goes here
}
每當定義新的結構或類時,就定義新的 Swift 型別。命名時使用 大駝峰式命名法 (UpperCamelCase) 來定義新型別(例如此處的 SomeStructure 和 SomeClass)以匹配標準 Swift 型別(例如 String、Int 和 Bool)的大小寫。而屬性和方法則使用小駝峰式命名法 (lowerCamelCase) 命名規則(例如 frameRate 和 incrementCount)用來辨別它們與型別名稱的不同。
以下是一個結構定義和類定義的範例:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
上面的範例定義了一種稱為 Resolution
的新結構,用於描述基於像素的顯示分辨率。該結構具有兩個儲存的屬性,稱為 width
和 height
。儲存的屬性是常數或變數,它們捆綁在一起並儲存為結構或類的一部分。透過將它們設置為初始整數值 0,可以推斷這兩個屬性為 Int 型別。
上面的範例還定義了一個稱為 VideoMode
的新類,描述用於視訊顯示的特定視訊模式。此類具有四個變數儲存的屬性。第一個分辨率是使用新的 Resolution
結構實例初始化的,該實例可以推斷 Resolution
的屬性型別。對於其他三個屬性,新的 VideoMode
實例將 interlaced
設置 false
(意為「非隔行視訊」),播放幀率為 0.0 以及名為 name
的可選型別 String 值進行初始化。名稱屬性會自動被賦予預設值 nil
或「無名稱值」,因為它是可選型別。
Resolution 結構定義和 VideoMode 類定義僅描述Resolution 或 VideoMode 的樣子。它們本身並沒有描述特定的分辨率或視頻模式。為此,則需要創建結構或類的實例。
對於結構和類建立實例的語法非常類似:
let someResolution = Resolution()
let someVideoMode = VideoMode()
結構和類皆使用初始化器語法用於新實例。初始化器語法最簡單的形式是在類或結構的名稱後面加上內部沒有參數的小括號,例如:Resolution()
或 VideoMode()
。這樣就創建了一個類或結構的新實例,並將任何屬性初始化為其預設值。
可以使用點語法存取實例的屬性。在點語法中,應在實例名稱之後立即寫上屬性名稱,並用句點 (.) 分隔,不能有任何空格:
print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0"
在此範例中,someResolution.width
引用someResolution
的屬性 width
,並返回其預設初始值 0。
可以往下進一步了解子屬性,例如 VideoMode
的屬性 resolution
中的屬性 width
:
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0"
還可以使用點語法將新值指定給變數屬性:
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is now 1280"
所有結構都有一個自動生成的成員初始化器,可以使用該初始化器來初始化新結構實例的成員屬性。可以通過名稱將新實例的屬性的初始值傳遞給成員初始化器:
let vga = Resolution(width: 640, height: 480)
與結構不同,類實例不會接收預設的成員初始化器。
值型別是一種其值在被賦值給變數或常數時被複製,或者在傳遞給函數時被複製。實際上,Swift 的整數、浮點數、布林值、字符串、數組和字典中的所有基本型別都是值型別,並且在幕後實現為結構。所有結構和列舉都是 Swift 中的值型別。這代表著我們創建的任何結構和列舉實例以及它們作為屬性的任何值類型在代碼中傳遞時始終會被複製。
由標準庫(例如數組、字典和字串)定義的集合使用優化來降低複製的性能成本。這些集合不立即製作副本,而是共享儲存在原始實例與任何副本之間的元素的內存。如果修改了集合的副本之一,則在修改之前就將元素複製。在代碼中看到的行為始終就像是立即進行了複製一樣。
以下為範例:
struct Resolution {
var width = 0
var height = 0
}
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
cinema.width = 2048
print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide"
print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide"
由上述例子可知,當 cinema
被賦予 hd
當前的值,儲存在 hd
的值被複製到新 cinema
的實例。最後結果兩個擁有相同值但完全不同的實例,所以當修改 cinema.width = 2048
的時候,並不會影響儲存在 hd
中的 width
。
相同的行為適用於列舉:
enum CompassPoint {
case north, south, east, west
mutating func turnNorth() {
self = .north
}
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()
print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// Prints "The current direction is north"
// Prints "The remembered direction is west"
RememberedDirection
指定 currentDirection
的值時,它實際上是設置為該值的副本。此後更改 currentDirection
的值不會影響儲存在 RememberedDirection
中原始值的副本。
不同於值型別,當參考型別被指定給一個變數或常數,或是傳遞進一個函數的時候,並不會被複製。參考型別非副本,是使用相同存在的實例。
以下為範例:
class VideoMode {
var interlaced = false
var frameRate = 0.0
var name: String?
}
let tenEighty = VideoMode()
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0"
由上述例子可知,當 alsoTenEighty
被指定為 tenEighty
,並且修改 alsoTenEighty.frameRate = 30.0
時,也會更動到 tenEighty.frameRate
的值。
由於類是參考型別,因此多個常數和變數可能會在後台參考同一類的單個實例。(對於結構和列舉,情況並非如此,因為在將它們分配給常數或變數或傳遞給函數時,它們總是被複製。)
有時找出兩個常數或變數是否參考類的完全相同實例有時會很有用。為此,Swift 提供了兩個特性運算符:
使用者兩個運算符檢查兩個常數或變數是否參考相同的單個實例:
if tenEighty === alsoTenEighty {
print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."
請注意,相同 (identical)(用三個等號或 “===” 表示)並不代表等於 (equal)(用兩個等號或 “==” 表示)。相同表示類型別的兩個常數或變數參考完全相同的類實例。等於表示兩個實例視為相同或值相等。
結構和類是在應用程序中儲存數據和建模行為的不錯選擇,但是結構和類的相似性可能使得很難選擇一個。
考慮以下建議,來幫助選擇在向應用程序中添加新數據型別時哪個選項有意義。
以下針對這四種建議進行討論:
使用結構表示常見型別的數據。 Swift 中的結構包含許多功能,在其他語言中這些功能僅限於類:它們可以包含儲存屬性、計算屬性和方法。此外,Swift 結構可以採用協定來通過預設實現獲得行為。Swift 標準庫和 Foundation 經常使用的型別為結構,例如數字、字串、數組和字典。
使用結構可以更輕鬆地推理代碼的一部分,而無需考慮應用程序的整個狀態。因為結構是值型別(與類不同),所以對結構的局部更改對應用程序的其餘部分不可見,除非有意將這些更改作為應用程序流程的一部分進行傳達。結果,可以查看一段代碼,並更有信心將對該部分中的實例進行更改,而不是通過與相關的函數調用進行不可見的更改。
如果使用需要處理數據的 Objective-C API,或者需要使數據模型適合 Objective-C 框架中定義的現有類層次結構,則可能需要使用類和類繼承來對數據進行建模。例如,許多 Objective-C 框架都公開了子類化的類。
Swift 中的類帶有內置的特性概念,因為它們是參考型別。這代表當兩個不同的類實例的每個儲存屬性具有相同的值時,特性運算符(===)仍將它們視為不同。這也代表著,當在應用中共享一個類實例時,對該實例所做的更改對於代碼中參考該實例都是可見的。當需要實例具有這種特性時,請使用類。常見的用例是文件處理、網絡連接以及共享的硬體媒介。
例如,如果有一個表示本地數據庫連接的型別,則管理對數據庫訪問權限的代碼需要完全控制數據庫狀態。在這種情況下,使用一個類是適當的,但是一定要限制應用程序的哪些部分可以訪問共享數據庫物件。
重要
處理特性時應注意。在應用程序中普遍共享類實例,更有可能發生邏輯錯誤。可能無法預料到更改大量共享實例的後果,因此,正確編寫此類代碼需要花更多的心力。
在對包含具有不受控制設定的實體的訊息的數據進行建模時,請使用結構。
例如,在查詢遠程數據庫的應用中,實例的特性可能完全由外部實體擁有,並由標識符傳達。如果應用程序模型的一致性存儲在服務器上,則可以將記錄建模為帶有標識符的結構。在下面的範例中, jsonResponse
包含來自服務器的編碼 PenPalRecord
實例:
struct PenPalRecord {
let myID: Int
var myNickname: String
var recommendedPenPalID: Int
}
var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)
對於如 PenPalRecord
之類的模型型別進行本地更改非常有用。例如,一個應用可能會響應用戶反饋而推薦多個不同的筆友。由於 PenPalRecord
結構不控制基礎數據庫記錄的特性,因此,對本地 PenPalRecord
實例所做的更改不會意外地更改數據庫中的值,則不會有風險。
如果應用程序的另一部分更改了 myNickname
並將更改請求提交回服務器,則最近拒絕的筆友建議並不會被接受。由於 myID
屬性宣告為常數,因此無法在本地更改。因此,對數據庫的請求不會意外更改錯誤的記錄。
結構和類都支持一種繼承形式。結構和協定只能採用協定;他們不能從類繼承。但是,使用類繼承建構的繼承階層可以使用協定繼承和結構進行建模。
如果要從頭開始建立繼承關係,則最好使用協定繼承。協定允許類、結構和列舉參與繼承,而類繼承僅與其他類兼容。當選擇如何對數據建模時,請先嘗試使用協定繼承構建數據類型的層次結構,然後在結構中採用這些協定。