8、Swift類和結構體
結構體和類作為一種通用而又靈活的結構,成為了人們構建程式碼的基礎。你可以使用定義常量、變數和函式的語法,為你的結構體和類定義屬性、新增方法
與其他程式語言所不同的是,Swift 並不要求你為自定義的結構體和類的介面與實現程式碼分別建立檔案。你只需在單一的檔案中定義一個結構體或者類,系統將會自動生成面向其它程式碼的外部介面。
結構體和類對比
共同點:
- 定義屬性用於儲存值
- 定義方法用於提供功能
- 定義下標操作用於通過下標語法訪問它們的值
- 定義構造器用於設定初始值
- 通過擴充套件以增加預設實現之外的功能
- 遵循協議以提供某種標準功能
與結構體相比,類還有如下的附加功能:
- 繼承允許一個類繼承另一個類的特徵
- 型別轉換允許在執行時檢查和解釋一個類例項的型別
- 析構器允許一個類例項釋放任何其所被分配的資源
- 引用計數允許對一個類的多次引用
型別定義的語法
結構體和類有著相似的定義方式。你通過 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
結構體和 VideoMode
類的定義僅描述了什麼是 Resolution
和 VideoMode
。它們並沒有描述一個特定的解析度(resolution
)或者視訊模式(video mode
)。為此,你需要建立結構體或者類的一個例項。
建立結構體和類例項的語法非常相似:
let someResolution = Resolution()
let someVideoMode = VideoMode()
結構體和類都使用構造器語法來建立新的例項。構造器語法的最簡單形式是在結構體或者類的型別名稱後跟隨一對空括號,如 Resolution() 或 VideoMode()。通過這種方式所建立的類或者結構體例項,其屬性均會被初始化為預設值。構造過程 章節會對類和結構體的初始化進行更詳細的討論。
屬性訪問
你可以通過使用點語法訪問例項的屬性。其語法規則是,例項名後面緊跟屬性名,兩者以點號(.)分隔,不帶空格:
print("The width of someResolution is \(someResolution.width)")
// 列印 "The width of someResolution is 0"
結構體型別的成員逐一構造器
所有結構體都有一個自動生成的成員逐一構造器,用於初始化新結構體例項中成員的屬性。新例項中各個屬性的初始值可以通過屬性的名稱傳遞到成員逐一構造器之中:
let vga = Resolution(width: 640, height: 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
。儘管 hd
和 cinema
有著相同的寬(width
)和高(height
),但是在幕後它們是兩個完全不同的例項。
下面,為了符合數碼影院放映的需求(2048
畫素寬,1080
畫素高),cinema
的 width
屬性被修改為稍微寬一點的 2K
標準:
cinema.width = 2048
檢視 cinema
的 width
屬性,它的值確實改為了 2048
:
print("cinema is now \(cinema.width) pixels wide")
// 列印 "cinema is now 2048 pixels wide"
然而,初始的 hd
例項中 width
屬性還是 1920
:
print("hd is still \(hd.width) pixels wide")
// 列印 "hd is still 1920 pixels wide"
將 hd
賦值給 cinema
時,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)")
// 列印 "The current direction is north"
// 列印 "The remembered direction is west"
類是引用型別
與值型別不同,引用型別在被賦予到一個變數、常量或者被傳遞到一個函式時,其值不會被拷貝。因此,使用的是已存在例項的引用,而不是其拷貝。
請看下面這個示例,其使用了之前定義的 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
因為類是引用型別,所以 tenEight
和 alsoTenEight
實際上引用的是同一個 VideoMode
例項。換句話說,它們是同一個例項的兩種叫法,如下圖所示:
通過檢視 tenEighty
的 frameRate
屬性,可以看到它正確地顯示了底層的 VideoMode
例項的新幀率 30.0
:
print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 列印 "The frameRate property of theEighty is now 30.0"
這個例子也顯示了為何引用型別更加難以理解。如果 tenEighty
和 alsoTenEighty
在你程式碼中的位置相距很遠,那麼就很難找到所有修改視訊模式的地方。無論在哪使用 tenEighty
,你都要考慮使用 alsoTenEighty
的程式碼,反之亦然。相反,值型別就更容易理解了,因為你的原始碼中與同一個值互動的程式碼都很近。
需要注意的是 tenEighty
和 alsoTenEighty
被宣告為常量而不是變數。然而你依然可以改變 tenEighty.frameRate
和 alsoTenEighty.frameRate
,這是因為 tenEighty
和 alsoTenEighty
這兩個常量的值並未改變。它們並不“儲存”這個 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."
請注意,“相同”(用三個等號表示,===
)與“等於”(用兩個等號表示,==
)的不同。“相同”表示兩個類型別(class type)的常量或者變數引用同一個類例項。“等於”表示兩個例項的值“相等”或“等價”,判定時要遵照設計者定義的評判標準。
當在定義你的自定義結構體和類的時候,你有義務來決定判定兩個例項“相等”的標準。在章節 等價操作符 中將會詳細介紹實現自定義 == 和 != 運算子的流程。
指標
如果你有 C,C++ 或者 Objective-C 語言的經驗,那麼你也許會知道這些語言使用指標來引用記憶體中的地址。Swift 中引用了某個引用型別例項的常量或變數,與 C 語言中的指標類似,不過它並不直接指向某個記憶體地址,也不要求你使用星號(*)來表明你在建立一個引用。相反,Swift 中引用的定義方式與其它的常量或變數的一樣。如果需要直接與指標互動,你可以使用標準庫提供的指標和緩衝區型別 —— 參見 手動管理記憶體。