結(jié)構(gòu)體和類作為一種通用而又靈活的結(jié)構(gòu),成為了人們構(gòu)建代碼的基礎(chǔ)。你可以使用定義常量、變量和函數(shù)的語法,為你的結(jié)構(gòu)體和類定義屬性、添加方法。
與其他編程語言所不同的是,Swift 并不要求你為自定義的結(jié)構(gòu)體和類的接口與實現(xiàn)代碼分別創(chuàng)建文件。你只需在單一的文件中定義一個結(jié)構(gòu)體或者類,系統(tǒng)將會自動生成面向其它代碼的外部接口。
注意
通常一個類的實例被稱為對象。然而相比其他語言,Swift 中結(jié)構(gòu)體和類的功能更加相近,本章中所討論的大部分功能都可以用在結(jié)構(gòu)體或者類上。因此,這里會使用實例這個更通用的術(shù)語。
Swift 中結(jié)構(gòu)體和類有很多共同點。兩者都可以:
更多信息請參見 屬性、方法、下標(biāo)、構(gòu)造過程、擴展 和 協(xié)議。
與結(jié)構(gòu)體相比,類還有如下的附加功能:
更多信息請參見 繼承、類型轉(zhuǎn)換、析構(gòu)過程 和 自動引用計數(shù)。
類支持的附加功能是以增加復(fù)雜性為代價的。作為一般準(zhǔn)則,優(yōu)先使用結(jié)構(gòu)體,因為它們更容易理解,僅在適當(dāng)或必要時才使用類。實際上,這意味著你的大多數(shù)自定義數(shù)據(jù)類型都會是結(jié)構(gòu)體和枚舉。更多詳細的比較參見 在結(jié)構(gòu)和類之間進行選擇。
結(jié)構(gòu)體和類有著相似的定義方式。你通過 struct
關(guān)鍵字引入結(jié)構(gòu)體,通過 class
關(guān)鍵字引入類,并將它們的具體定義放在一對大括號中:
struct SomeStructure {
// 在這里定義結(jié)構(gòu)體
}
class SomeClass {
// 在這里定義類
}
注意
每當(dāng)你定義一個新的結(jié)構(gòu)體或者類時,你都是定義了一個新的 Swift 類型。請使用
UpperCamelCase
這種方式來命名類型(如這里的SomeClass
和SomeStructure
),以便符合標(biāo)準(zhǔn) Swift 類型的大寫命名風(fēng)格(如String
,Int
和Bool
)。請使用lowerCamelCase
這種方式來命名屬性和方法(如frameRate
和incrementCount
),以便和類型名區(qū)分。
以下是定義結(jié)構(gòu)體和定義類的示例:
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
在上面的示例中定義了一個名為 Resolution
的結(jié)構(gòu)體,用來描述基于像素的分辨率。這個結(jié)構(gòu)體包含了名為 width
和 height
的兩個存儲屬性。存儲屬性是與結(jié)構(gòu)體或者類綁定的,并存儲在其中的常量或變量。當(dāng)這兩個屬性被初始化為整數(shù) 0
的時候,它們會被推斷為 Int
類型。
在上面的示例還定義了一個名為 VideoMode
的類,用來描述視頻顯示器的某個特定視頻模式。這個類包含了四個可變的存儲屬性。第一個, resolution
,被初始化為一個新的 Resolution
結(jié)構(gòu)體的實例,屬性類型被推斷為 Resolution
。新 VideoMode
實例同時還會初始化其它三個屬性,它們分別是初始值為 false
的 interlaced
(意為“非隔行視頻”),初始值為 0.0
的 frameRate
,以及值為可選 String
的 name
。因為 name
是一個可選類型,它會被自動賦予一個默認值 nil
,意為“沒有 name
值”。
Resolution
結(jié)構(gòu)體和 VideoMode
類的定義僅描述了什么是 Resolution
和 VideoMode
。它們并沒有描述一個特定的分辨率(resolution)或者視頻模式(video mode)。為此,你需要創(chuàng)建結(jié)構(gòu)體或者類的一個實例。
創(chuàng)建結(jié)構(gòu)體和類實例的語法非常相似:
let someResolution = Resolution()
let someVideoMode = VideoMode()
結(jié)構(gòu)體和類都使用構(gòu)造器語法來創(chuàng)建新的實例。構(gòu)造器語法的最簡單形式是在結(jié)構(gòu)體或者類的類型名稱后跟隨一對空括號,如 Resolution()
或 VideoMode()
。通過這種方式所創(chuàng)建的類或者結(jié)構(gòu)體實例,其屬性均會被初始化為默認值。構(gòu)造過程 章節(jié)會對類和結(jié)構(gòu)體的初始化進行更詳細的討論。
你可以通過使用點語法訪問實例的屬性。其語法規(guī)則是,實例名后面緊跟屬性名,兩者以點號(.
)分隔,不帶空格:
print("The width of someResolution is \(someResolution.width)")
// 打印 "The width of someResolution is 0"
在上面的例子中,someResolution.width
引用 someResolution
的 width
屬性,返回 width
的初始值 0
。
你也可以訪問子屬性,如 VideoMode
中 resolution
屬性的 width
屬性:
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is 0"
你也可以使用點語法為可變屬性賦值:
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// 打印 "The width of someVideoMode is now 1280"
所有結(jié)構(gòu)體都有一個自動生成的成員逐一構(gòu)造器,用于初始化新結(jié)構(gòu)體實例中成員的屬性。新實例中各個屬性的初始值可以通過屬性的名稱傳遞到成員逐一構(gòu)造器之中:
let vga = Resolution(width: 640, height: 480)
與結(jié)構(gòu)體不同,類實例沒有默認的成員逐一構(gòu)造器。構(gòu)造過程 章節(jié)會對構(gòu)造器進行更詳細的討論。
值類型是這樣一種類型,當(dāng)它被賦值給一個變量、常量或者被傳遞給一個函數(shù)的時候,其值會被拷貝。
在之前的章節(jié)中,你已經(jīng)大量使用了值類型。實際上,Swift 中所有的基本類型:整數(shù)(integer)、浮點數(shù)(floating-point number)、布爾值(boolean)、字符串(string)、數(shù)組(array)和字典(dictionary),都是值類型,其底層也是使用結(jié)構(gòu)體實現(xiàn)的。
Swift 中所有的結(jié)構(gòu)體和枚舉類型都是值類型。這意味著它們的實例,以及實例中所包含的任何值類型的屬性,在代碼中傳遞的時候都會被復(fù)制。
注意
標(biāo)準(zhǔn)庫定義的集合,例如數(shù)組,字典和字符串,都對復(fù)制進行了優(yōu)化以降低性能成本。新集合不會立即復(fù)制,而是跟原集合共享同一份內(nèi)存,共享同樣的元素。在集合的某個副本要被修改前,才會復(fù)制它的元素。而你在代碼中看起來就像是立即發(fā)生了復(fù)制。
請看下面這個示例,其使用了上一個示例中的 Resolution
結(jié)構(gòu)體:
let hd = Resolution(width: 1920, height: 1080)
var cinema = hd
在以上示例中,聲明了一個名為 hd
的常量,其值為一個初始化為全高清視頻分辨率(1920
像素寬,1080
像素高)的 Resolution
實例。
然后示例中又聲明了一個名為 cinema
的變量,并將 hd
賦值給它。因為 Resolution
是一個結(jié)構(gòu)體,所以會先創(chuàng)建一個現(xiàn)有實例的副本,然后將副本賦值給 cinema
。盡管 hd
和 cinema
有著相同的寬(width)和高(height),但是在幕后它們是兩個完全不同的實例。
下面,為了符合數(shù)碼影院放映的需求(2048
像素寬,1080
像素高),cinema
的 width
屬性被修改為稍微寬一點的 2K 標(biāo)準(zhǔn):
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
實例中。結(jié)果就是兩個完全獨立的實例包含了相同的數(shù)值。由于兩者相互獨立,因此將 cinema
的 width
修改為 2048
并不會影響 hd
中的 width
的值,如下圖所示:
枚舉也遵循相同的行為準(zhǔn)則:
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"
當(dāng) rememberedDirection
被賦予了 currentDirection
的值,實際上它被賦予的是值的一個拷貝。賦值過程結(jié)束后再修改 currentDirection
的值并不影響 rememberedDirection
所儲存的原始值的拷貝。
與值類型不同,引用類型在被賦予到一個變量、常量或者被傳遞到一個函數(shù)時,其值不會被拷貝。因此,使用的是已存在實例的引用,而不是其拷貝。
請看下面這個示例,其使用了之前定義的 VideoMode
類:
let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
以上示例中,聲明了一個名為 tenEighty
的常量,并讓其引用一個 VideoMode
類的新實例。它的視頻模式(video mode)被賦值為之前創(chuàng)建的 HD 分辨率(1920
*1080
)的一個拷貝。然后將它設(shè)置為隔行視頻,名字設(shè)為 “1080i”
,并將幀率設(shè)置為 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
的常量引用的值。
因為類是引用類型,所以多個常量和變量可能在幕后同時引用同一個類實例。(對于結(jié)構(gòu)體和枚舉來說,這并不成立。因為它們作為值類型,在被賦予到常量、變量或者傳遞到函數(shù)時,其值總是會被拷貝。)
判定兩個常量或者變量是否引用同一個類實例有時很有用。為了達到這個目的,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)的常量或者變量引用同一個類實例?!暗扔凇北硎緝蓚€實例的值“相等”或“等價”,判定時要遵照設(shè)計者定義的評判標(biāo)準(zhǔn)。
當(dāng)在定義你的自定義結(jié)構(gòu)體和類的時候,你有義務(wù)來決定判定兩個實例“相等”的標(biāo)準(zhǔn)。在章節(jié) 等價操作符 中將會詳細介紹實現(xiàn)自定義 == 和 != 運算符的流程。
如果你有 C,C++ 或者 Objective-C 語言的經(jīng)驗,那么你也許會知道這些語言使用指針來引用內(nèi)存中的地址。Swift 中引用了某個引用類型實例的常量或變量,與 C 語言中的指針類似,不過它并不直接指向某個內(nèi)存地址,也不要求你使用星號(*
)來表明你在創(chuàng)建一個引用。相反,Swift 中引用的定義方式與其它的常量或變量的一樣。如果需要直接與指針交互,你可以使用標(biāo)準(zhǔn)庫提供的指針和緩沖區(qū)類型 —— 參見 手動管理內(nèi)存。