Swift 程式語言 — Structures & Classes

讓我們來討論編成程式碼中重要的結構。

Jeremy Xue
Jeremy Xue ‘s Blog
14 min readApr 29, 2019

--

Photo by Kelly Sikkema on Unsplash

# 前言:

Structures (結構) Classes (類)是通用的,靈活的結構使他們成為程式碼的建構塊,你可以使用與定義常數、變數和函數相同的方式來定義屬性和方法,以便為 Structures 和 Classes 添加功能。

與其他程式語言不同,Swift 不會要求你為自定義的 structures 或 classes 去創建單獨的接口或實做檔案。在 Swift 中,你可以在單個檔案中去定義一個 structures 或 classes ,系統自動使該 structures 或 classes 的外部接口可供其他程式碼能夠使用。

傳統上,Classes 的實例被稱作為 Object(對象)。然而,Swift 的 structures 和 classes 在功能上比其他語言更接近。

# Strcutures 和 Classes 的比較

Swift 中的 structures 和 classes 有許多共同點,其兩者都能夠:

  • 定義屬性用來儲存值
  • 定義方法用來提供功能
  • 定義下標用來提供使用下標語法訪問其值
  • 定義初始化器用於設置其初始狀態
  • 擴展用來擴展其功能,超出默認的實現
  • 符合協議(Protocols)用於提供某些標準功能

而 Classes 還具有 Structures 不具備的功能:

  • 繼承(Inheritance)使某個 class 去繼承另一個 class 的特性
  • 類型轉換(Type casting)使你能夠在運行時檢查和解釋 class 實例的類型
  • 反初始化器(Deinitializers)使一個 class 的實例能夠被釋放任何它被分配的資源
  • 引用技術(Reference counting)允許對一個 class 實例的多個引用

而 classes 支援的額外功能增加複雜性為代價。作爲一般準則,更偏向使用 structures,因為他們更容易推理,並在適當或是必要時使用 classes。實際上,這意味著你定義的大多數自定義的資料類型都會是 structures 和 enumerations。

更多詳細的比較可以參考「 Choosing Between Structures and Classes 」這篇官方文檔。

# 定義語法

Structures 和 Classes 有著類似的定義語法。你可以透過 struct 關鍵字定義 structures ;使用 class 關鍵字定義 classes,並且將他們整個定義放在一對大括弧中:

struct SomeStructure {
// 定義結構內容
}
class SomeClass {
// 定義類內容
}

無論你在何時定義了一個新的 structures 或是 classes,都是定義了一個新的 Swift 類型。

  • 給予類型 UpperCamelCase 名稱( 例如 SomeStruct、SomeClass)來匹配標準的 Swift 類型(例如:String 和 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?
}

#Structures 和 Classes 實例

Resolution 結構和 VideoMode 類的定義僅僅描述了什麼是 Resolution 和 VideoMode。它們對自己並沒有一個特定的描述。因此,你需要創建一個他們的實例,對於 Structures 和 Classes,其創建實例的語法也是非常相似:

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

Structures 和 Classes 都能使用初始化器語法用於新的實例。最簡單的初始化方式就是使用 structure 或是 class 的類型名稱,在其後方加上空括號( 例如:Resolution()VideoMode())。這將會創建一個新的 structure 或 class 的實例,並將其任何屬性初始化為它們的默認值。

定義 Structures 和 Classes 的內容像是在畫設計圖,而創建它們實例才是真正將它們創造出來。

# 訪問屬性

你可以使用點語法來訪問實例的屬性。在點語法中,你可以在實例名稱後方使用點(.)加上屬性名稱,來訪問該屬性:

訪問 someResolution 的 width 屬性

在這個例子中, someResolution.width 就是 someResolution 中的 width 屬性,返回其的默認值 0 。

您還可以深入查看子屬性,例如訪問 VideoMode 實例中的 resolution 屬性的 width 屬性:

訪問 someVideoMode 中的 resolution 屬性中的 width 屬性

你也可以使用點語法為變數屬性分配新值:

修改 someVideoMode 中的 resolution 屬性中的 width 變數屬性

# Structures 類型的成員初始化器

所有的 Structures 都有一個自動生成的成員初始化器( Memberwise initializer ),你可以使用它初始化新結構實例中的成員屬性:

結構類型自動生成的成員初始化器( Memberwise initializer )

新實例屬性的初始化值可以通過屬性名稱傳遞到成員初始化器中:

通過屬性名稱初始化值

與 Structures 不同,Class 實例不會接收默認的成員初始化器。

# Structures 和 Enumerations 為值類型

值類型( Value Type )是一種類型,其值在被賦值給變數或常數時被複製,或者在傳遞給函數時被複製。

你實際上已經廣泛地使用過值類型。Swift 中的所有基本類型 — 整數、福點數、布爾值、字串、數組和字典中的都是值類型,並且以結構的形式在背後實現。

所有的 Structures 和 Enumerations 在 Swift 中都為值類型。這意味著你創建的所有結構和枚舉的實例和他們作為屬性的任何值類型在程式碼中傳遞時始終會被複製。

標準庫( 像是 Array、Dictionary 和 String )定義的集合使用優化來降低複製的效能成本。這些集合不是立即複製,而是共享內存,其中的元素儲存在原始實例和任何副本之間。如果修改了集合的其中一個副本,則在修改之前複製元素。你在程式碼中看到的行為總是好像立即發生了複製。

看這個範例,它使用上一個範例中的 Resolution 結構:

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

這個範例宣告了一個稱為 hd 的常數,並且賦予它一個以全高清視頻( 1920 像素寬乘以 1080 像素高)寬和高初始化的 Resolution 實例。

接著我們宣告一個名為 cinema 的變數,並且將其設置為當前的 hd 值。由於 Resolution 是一個結構,因此會 生成現有實例的副本,並且將此新的副本賦給 cinema。儘管 hdcinema 現在具有相同的寬高,但是他們在背後是兩個完全不同的實例。

接下來,將 cinmeawidth 屬性修改為用於數位電影投影( 2048像素寬和1080像素高 )的稍為寬一點的 2K 標準的寬度,接著來檢查 cinmeawidth 屬性表明它確實變為 2048:

cinema 的 width 已經變為 2048

但是,原始的 hd 實例的 width 屬性仍具有舊值 1920:

原有的 hd 實例的 width 依然為 1920

cinema 被賦予 hd 的當前值時,儲存在 hd 中的值被複製到新的 cinema 實例中。最終結果是包含相同數值的兩個完全獨立的實例。但是,因為他們是單獨的實例,所以將 cinemawidth 設置為 2048 不會影響 hd 中儲存的 width,如下圖:

圖片來源: https://docs.swift.org/swift-book/_images/sharedStateStruct_2x.png

同樣的行為適用於枚舉。我們下面定義一個 CompassPoint 的結構,並且其中有一個可變函數( mutating function )將自身的值改變為 .north

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

接著我們新增一個名為 currentDirection 的實例,並且將其當前值賦予給一個新的 rememberedDirection 的常數:

var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection

接著我們讓 currentDirection 執行 turnNorth 方法,將其自身的值改變為 .north ,接著我們看看是否與上面範例的結果相同:

rememberedDirection 被分配 currentDirection 的當前值,他實際上被設置為該值的副本。此後更改 currentDirection 的值不會影響到儲存在 rememberedDirection 的原始值的副本。

# Classes 為引用類型

不同於值類型,引用類型( Reference Types )再分配給常數或變數或是傳遞給函數時不會被複製。使用對同一個現有的實例的引用,而不是副本。

這邊我們使用上面所定義的 VideoMode 類作為範例:

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

這邊我們宣告一個名為 tenEighty 的變數,並且將其設置為引用 VideoMode 類的新實例。 影像模式從之前分配了 1920×1080 的 HD 分辨率的副本。 它被設置為隔行掃描,其名稱設置為 1080i,其幀率設置為每秒 25.0 幀。

接著,我們將 tenEighty 分配給一個名為 alsoTenEighty 的常數,並且修改 alsoTenEighty 中的幀率:

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

因為 Classes 是引用類型,所以 tenEightyalsoTenEighty 實際上都引用了同一個 VideoMode 實例。實際上,它們只是同一個實例的兩個不同名稱,如下圖所示:

圖片來源:https://docs.swift.org/swift-book/_images/sharedStateClass_2x.png

接著我們來檢查 tenEightyframeRate 屬性來表示它正確地從基礎 VideoMode 實例反映出新的幀率 30.0:

這個範例同時顯示出了為什麼引用類型更難以推理。如果 tenEightyalsoTenEighty 在你的程式碼中相距甚遠,那麼可能很難去找到更改影像模式的所有方式。無論你是否使用 tenEighty,你還必須考慮到使用到 alsoTenEighty 的程式碼,反之亦然。

相反,值類型更容易推理,因為與相同值交互的所有程式碼在原文件中都是緊密相連的

注意,tenEightyalsoTenEighty 被宣告為常數。但是,你仍然可以更改 tenEighty.frameRatealsoTenEighty.frameRate因為 tenEightyalsoTightEighty 常數的值本身並未實際被更改,而是其中的變數屬性 frameRate 被更改了

無法在賦予給 alsoTenEighty 新的 VideoMode 實例

tenEightyalsoTenEighty 本身不 “儲存” VideoMode 的實例 — 相反的,他們都引用了背後 VideoMode 的實例。他是底層的 VideoModeframeRate 屬性被更改,而不是對該 VideoMode 的常數引用的值。

# 身份運算符( Identity operators )

因為 Classes 為引用類型,所以多個常數和變數可以在背後引用同一個 Class 的實例。( Structures 和 Enumerations 也是如此,因為當它們再分配給常數或變數或是傳遞給函數時總是被複製。)

有時找出兩個常數或是變數是否引用 Class 的相同實例,是非常有用的。為了實現這一點,Swift 提供了兩個身份運算符:

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

我們使用身份運算符來檢查兩個常數或是變數是否引用同一個實例:

使用身份運算符判斷結果:tenEighty 和 alsoTenEighty 引用同一個實例

注意,相同(===)並不意味著等於的東西(==)

相同( idetical )意味著 class 類型的兩個常數或是變數引用完全相同的 class 實例,而等於( equal )意味著兩個實例在值上被認為是相等或是等價的,對於某些適當的相等含義,由該類型的設計者所定義。

當你定義了自定義的 Structures 或 Classes 時,你有責任決定兩個實例的相同條件。在等價運算符(Equivalence Operators)中描述了定義自己的(==)(!=)運算符實現的過程。

# 指針( Pointers )

如果你有 C、C++ 或 Objective-C 的經驗,你可能知道這些語言使用 “指針” 來引用內存中的地址(Addresses)。Swift 中引用某個引用類型的實例的常數或變數類似於 C 語言中的指針,但他不是只向內存中地址的直接指針,並且需要你編寫星號(*)來表示你正在創建一個引用。

相反,這些引用的定義與 Swift 中的任何常數和變數相同。標準庫提供指針和緩衝區類型,如果需要指針交互,可以使用它們。

# 後記:

對於 Classes 與 Structures 的介紹就到這邊結束了,這兩個類型通常是剛接觸的人常常會模糊不清的點,不曉得何時應該使用 Classes 還是 Structures,而 其中 Classes 也擁有 Structures 不具有的特性,所以可以進行更多複雜的操作。

但是如果深入了解兩者的特性後,我相信應該能夠很容易去決定該選擇何者。之後我也會參考 Choosing Between Structures and Classes 來編寫一篇文章,希望能夠幫助大家了解,我們下篇教學再見。

# 參考文章:

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]