Swift學習(11)- 類別與結構體(程式碼完善版)

Swift學習(11)- 類別與結構體(程式碼完善版)

August 30, 2021·Jingyao Zhang
Jingyao Zhang

image

結構體與類別作為一種通用且靈活的結構,成為了人們建構程式碼的基礎。可以使用定義常數、變數與函式的語法,為結構體與類別定義屬性、加入方法。

與其他程式語言不同的是,Swift並不要求為自訂的結構體與類別的介面與實作程式碼分別建立檔案。只需在單一檔案中定義結構體或類別,系統就會自動產生對其他程式碼可見的外部介面。

注意

通常一個類別的實例被稱為物件。然而相較於其他語言,Swift 中結構體與類別的功能更加接近,本章所討論的大部分內容都適用於結構體或類別。因此,這裡會使用「實例」這個更通用的術語。


結構體與類別比較

Swift 中,結構體與類別有許多共同點,兩者都可以:

  • 定義屬性以儲存值
  • 定義方法以提供功能
  • 定義下標操作以透過下標語法存取它們的值
  • 定義建構器以設定初始值
  • 透過擴充增加預設實作以外的功能
  • 遵循協定以提供某種標準功能 更多資訊請參見屬性方法下標建構過程擴充協定

與結構體相比,類別還有以下額外功能:

  • 繼承允許一個類別繼承另一個類別的特性
  • 型別轉換允許在執行時檢查與解釋類別實例的型別
  • 解構器允許類別實例釋放其所分配的任何資源
  • 參考計數允許對同一個類別多次參考 更多資訊請參見繼承型別轉換解構過程自動參考計數

類別支援的額外功能是以增加複雜度為代價。一般來說,優先使用結構體,因為它們較容易理解,僅在適當或必要時才使用類別。實際上,這代表大多數自訂資料型別會是結構體或列舉。

注意

類別與 actors 共享許多特性。

型別定義語法

結構體與類別有著相似的定義方式,透過 struct 關鍵字引入結構體,透過 class 關鍵字引入類別,並將它們的具體定義放在一對大括號中:

struct SomeStructure {
        // 在這裡定義結構體
}
class SomeClass {
        // 在這裡定義類別
}

注意

每當定義一個新的結構體或類別時,都是定義了一個新的 Swift 型別。請使用 UpperCamelCase 方式來命名型別(如這裡的 SomeClass 與 SomeStructure),以符合標準 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 的結構體,用來描述以像素為單位的解析度。這個結構體包含名為 widthheight 的兩個儲存屬性。儲存屬性是與結構體或類別綁定,並儲存在常數或變數中。當這兩個屬性初始化為整數 0 時,會被推斷為 Int 型別。

上述範例還定義了一個名為 VideoMode 的類別,用來描述顯示器的某個特定顯示模式。這個類別包含四個可變的儲存屬性。第一個 resolution 被初始化為一個新的 Resolution 結構體實例,屬性型別被推斷為 Resolution。新的 VideoMode 實例同時還會初始化其他三個屬性,分別是初始值為 falseinterlaced(意指「非交錯掃描」)、初始值為 0.0 的 frameRate,以及型別為可選 Stringname。由於 name 是可選型別,會自動被賦予預設值 nil,代表「沒有 name 值」。

結構體與類別的實例

Resolution 結構體與 VideoMode 類別的定義僅描述了什麼是 ResolutionVideoMode。它們並未描述特定的解析度(Resolution)或顯示模式(VideoMode)。為此,需要建立結構體或類別的實例。

let someResolution = Resolution()
let someVideoMode = VideoMode()

結構體與類別都使用建構器語法來建立新的實例。建構器語法最簡單的形式就是在型別名稱後加上一對空括號,如 Resolution()VideoMode()。透過這種方式建立的類別或結構體實例,其屬性都會被初始化為預設值。建構過程章節將更詳細說明類別與結構體的初始化。

屬性存取

可以透過點語法存取實例的屬性,語法規則為:實例名稱後接屬性名稱,中間以 . 分隔,不帶空格。

print("The width of someResolution is \(someResolution.width).")  // 輸出“The width of someResolution is 0.”
---
output: The width of someResolution is 0.

在上述例子中,someResolution.width 參照 someResolutionwidth 屬性,回傳 width 的初始值 0。

也可以存取子屬性,例如 someVideoModeresolution 屬性的 width 屬性:

print("The width of someVideoMode is \(someVideoMode.resolution.width).")  // 輸出“The width of someVideoMode is 0.”
---
output: The width of someVideoMode is 0.

也可以使用點語法為可變屬性賦值:

someVideoMode.resolution.width = 1280
print("Now, the width of someVideoMode is \(someVideoMode.resolution.width).")  // 輸出“Now, the width of someVideoMode is 1280.”
---
output: Now, the width of someVideoMode is 1280.

結構體型別的成員逐一建構器

所有結構體都有一個自動產生的成員逐一建構器,用於初始化新結構體實例中的成員屬性。新實例中各個屬性的初始值可以透過屬性名稱傳遞給成員逐一建構器:

let vga = Resolution(width: 640, height: 480)
print("The width of vga is \(vga.width) and the height is \(vga.height).")  // 輸出“The width of vga is 640 and the height is 480.”
---
output: The width of vga is 640 and the height is 480.

與結構體不同,類別實例沒有預設的成員逐一建構器。建構過程章節會更詳細說明建構器。


結構體與列舉是值型別

值型別是一種型別,當它被賦值給變數、常數或傳遞給參數時,其值會被複製。

在先前章節中,我們已大量使用值型別。事實上,Swift 中所有的基本型別:整數(Integer)、浮點數(Floating-Point Number)、布林值(Boolean)、字串(String)、陣列(Array)與字典(Dictionary)都是值型別,其底層也是使用結構體實作。

Swift 中所有的結構體與列舉都是值型別。這代表它們的實例,以及實例中所包含的任何值型別屬性,在程式中傳遞時都會被複製。

注意

標準函式庫定義的集合,如陣列、字典與字串,都對複製進行了最佳化以降低效能成本,新集合不會立即複製而是與原集合共用同一份記憶體、同樣的元素。在集合的某個副本要被修改之前,才會複製其元素。而開發者在程式碼中看起來就像是立即發生了複製。

請看下列範例,使用了前述的 Resolution 結構體:

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

在上述範例中宣告了一個名為 hd 的常數,其值初始化為全高清解析度(1920 像素寬,1080 像素高)的 Resolution 實例。

接著又宣告了一個名為 cinema 的變數,並將 hd 賦值給它。由於 Resolution 是結構體,所以會建立現有實例的副本,並將副本賦值給 cinema。雖然 hdcinema 有著相同的寬(Width)與高(Height),但在背後這兩個是完全不同的實例。

接下來,為了符合數位電影院的放映需求(2048 像素寬,1080 像素高),將 cinemawidth 屬性修改為較寬的 2K 標準:

cinema.width = 2048

查看 cinemawidth 屬性,其值確實被改為 2048:

print("The width of cinema is \(cinema.width).")  // 輸出“The width of cinema is 2048.”
---
output: The width of cinema is 2048.

然而,最初的 hdwidth 屬性值仍然是 1920:

print("The width of hd is still \(hd.width).")  // 輸出“The width of hd is still 1920.”
---
output: The width of hd is still 1920.

hd 賦值給 cinema 時,hd 中所儲存的值會複製到新的 cinema 實例中。結果就是兩個完全獨立的實例包含了相同的數值。由於兩者相互獨立,因此將 cinemawidth 修改為 2048 並不會影響 hd 中的 width 值,如下圖所示:

SharedState Struct

列舉也遵循相同的行為準則:

enum CompassPoint {
        case north, south, west, east
        mutating func turnNorth() {
                self = .north
        }
}
var currentDirection = CompassPoint.west
let rememberDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection).")  // 輸出“The current direction is north.”
print("The remember direction is \(rememberDirection).")  // 輸出“The remember direction is west.”
---
output: The current direction is north.
The remember direction is west.

rememberDirection 被賦予 currentDirection 的值時,實際上它被賦予的是一個值的複製,賦值結束後再修改 currentDirection 的值並不會影響 rememberDirection 所儲存的原始值的複製。


類別是參考型別

與值型別不同,參考型別被賦值給常數、變數或傳遞給函式時,其值不會被複製。因此使用的是已存在實例的參考而非其複製。

請看下列範例,使用了前面定義的 VideoMode 類別:

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

在上述範例中,宣告了一個名為 tenEighty 的常數,並讓它參考一個新的 VideoMode 類別實例。它的顯示模式(Video Mode)被賦值為先前建立的 HD 解析度(1920 * 1080)的一個複製,然後設定為交錯掃描,名稱為 “1080i”,並將影格率設為每秒 25.0 格。

接下來,將 tenEighty 賦值給一個名為 alsoTenEighty 的新常數,並修改 alsoTenEighty 的影格率:

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

由於類別是參考型別,所以 alsoTenEightytenEighty 參考的是同一個 VideoMode 實例。換句話說,它們是同一個實例的兩種叫法,如下圖所示:

SharedState Class

透過查看 tenEightyframeRate 屬性,可以看到其正確顯示底層 VideoMode 實例的新影格率 30.0:

print("The frameRate property of tenEighty is now \(tenEighty.frameRate).")  // 輸出“The frameRate property of tenEighty is now 30.0.”
---
output: The frameRate property of tenEighty is now 30.0.

這個例子也顯示了為什麼參考型別較難理解。如果 tenEightyalsoTenEighty 在程式碼中的位置相距甚遠,那麼就很難找到所有修改顯示模式的地方。無論在哪裡使用 tenEighty 都需要考慮 alsoTenEighty 的程式碼,反之亦然。相對地,值型別就容易理解許多,因為原始碼中,同一位置互動的程式碼都很接近。

需要注意的是,tenEightyalsoTenEighty 被宣告為常數而非變數。然而你仍然可以改變 tenEighty.frameRatealsoTenEighty.frameRate,這是因為 tenEightyalsoTenEighty 這兩個常數的值並未改變。它們並不「儲存」這個 VideoMode 實例,而僅僅是對 VideoMode 實例的參考。因此,改變的是底層 VideoMode 實例的 frameRate 屬性,而不是指向 VideoMode 的常數參考的值。

恒等運算子

由於類別是參考型別,所以多個常數與變數可能在背後同時參考同一個類別實例。(對於結構體與列舉來說,這並不成立。因為它們作為值型別,被賦值給常數、變數或傳遞給函式時,其值總是會被複製。)

判斷兩個常數或變數是否參考同一個類別實例時很有用。為此,Swift 提供了兩個恒等運算子:

  • 相同(===)
  • 不同(!==)

使用這兩個運算子檢查兩個常數或變數是否參考同一個實例:

if tenEighty === alsoTenEighty {
        print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")  // 輸出“tenEighty and alsoTenEighty refer to the same VideoMode instance.”
}
---
output: tenEighty and alsoTenEighty refer to the same VideoMode instance.

請注意,「相同」(用三個等號表示,===)與「等於」(用兩個等號表示,==)不同,「相同」表示兩個類別型別(class type)的常數或變數參考同一個類別實例。「等於」表示兩個實例的值「相等」或「等價」,判斷時要遵循設計者定義的標準。

當在定義自訂結構體與類別時,開發者有責任決定判斷兩個實例「相等」的標準。在章節進階運算子中將會詳細介紹實作自訂 ==!= 運算子的流程。

指標

如果有 CC++Objective-C 語言的開發經驗,大家也許會知道這些語言使用指標來參考記憶體中的位址。Swift 中參考某個參考型別實例的常數或變數,與 C 語言中的指標類似,不過它並不直接指向某個記憶體位址,也不需要使用 * 來表示正在建立一個參考。相反,Swift 中參考的定義方式與其他常數與變數一樣。如果需要直接與指標互動,可以使用標準函式庫提供的指標與緩衝區型別——請參見手動記憶體管理

最後更新於