這里的教程為Swift官方教程中文版。

結(jié)構(gòu)體和類

結(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ù)語。

結(jié)構(gòu)體和類對比

Swift 中結(jié)構(gòu)體和類有很多共同點。兩者都可以:

  • 定義屬性用于存儲值
  • 定義方法用于提供功能
  • 定義下標(biāo)操作用于通過下標(biāo)語法訪問它們的值
  • 定義構(gòu)造器用于設(shè)置初始值
  • 通過擴展以增加默認實現(xiàn)之外的功能
  • 遵循協(xié)議以提供某種標(biāo)準(zhǔn)功能

更多信息請參見 屬性、方法下標(biāo)、構(gòu)造過程、擴展協(xié)議。

與結(jié)構(gòu)體相比,類還有如下的附加功能:

  • 繼承允許一個類繼承另一個類的特征
  • 類型轉(zhuǎn)換允許在運行時檢查和解釋一個類實例的類型
  • 析構(gòu)器允許一個類實例釋放任何其所被分配的資源
  • 引用計數(shù)允許對一個類的多次引用

更多信息請參見 繼承、類型轉(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 這種方式來命名類型(如這里的 SomeClassSomeStructure),以便符合標(biāo)準(zhǔn) Swift 類型的大寫命名風(fēng)格(如 String,IntBool)。請使用 lowerCamelCase 這種方式來命名屬性和方法(如 frameRateincrementCount),以便和類型名區(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)體包含了名為 widthheight 的兩個存儲屬性。存儲屬性是與結(jié)構(gòu)體或者類綁定的,并存儲在其中的常量或變量。當(dāng)這兩個屬性被初始化為整數(shù) 0 的時候,它們會被推斷為 Int 類型。

在上面的示例還定義了一個名為 VideoMode 的類,用來描述視頻顯示器的某個特定視頻模式。這個類包含了四個可變的存儲屬性。第一個, resolution,被初始化為一個新的 Resolution 結(jié)構(gòu)體的實例,屬性類型被推斷為 Resolution。新 VideoMode 實例同時還會初始化其它三個屬性,它們分別是初始值為 falseinterlaced(意為“非隔行視頻”),初始值為 0.0frameRate,以及值為可選 Stringname。因為 name 是一個可選類型,它會被自動賦予一個默認值 nil,意為“沒有 name 值”。

結(jié)構(gòu)體和類的實例

Resolution 結(jié)構(gòu)體和 VideoMode 類的定義僅描述了什么是 ResolutionVideoMode。它們并沒有描述一個特定的分辨率(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 引用 someResolutionwidth 屬性,返回 width 的初始值 0。

你也可以訪問子屬性,如 VideoModeresolution 屬性的 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)造器,用于初始化新結(jié)構(gòu)體實例中成員的屬性。新實例中各個屬性的初始值可以通過屬性的名稱傳遞到成員逐一構(gòu)造器之中:

let vga = Resolution(width: 640, height: 480)

與結(jié)構(gòu)體不同,類實例沒有默認的成員逐一構(gòu)造器。構(gòu)造過程 章節(jié)會對構(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 。盡管 hdcinema 有著相同的寬(width)和高(height),但是在幕后它們是兩個完全不同的實例。

下面,為了符合數(shù)碼影院放映的需求(2048 像素寬,1080 像素高),cinemawidth 屬性被修改為稍微寬一點的 2K 標(biāo)準(zhǔn):

cinema.width = 2048

查看 cinemawidth 屬性,它的值確實改為了 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ù)值。由于兩者相互獨立,因此將 cinemawidth 修改為 2048 并不會影響 hd 中的 width 的值,如下圖所示:

sharedStateStruct_2x

枚舉也遵循相同的行為準(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

因為類是引用類型,所以 tenEightalsoTenEight 實際上引用的是同一個 VideoMode 實例。換句話說,它們是同一個實例的兩種叫法,如下圖所示:

sharedStateClass_2x

通過查看 tenEightyframeRate 屬性,可以看到它正確地顯示了底層的 VideoMode 實例的新幀率 30.0

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// 打印 "The frameRate property of theEighty is now 30.0"

這個例子也顯示了為何引用類型更加難以理解。如果 tenEightyalsoTenEighty 在你代碼中的位置相距很遠,那么就很難找到所有修改視頻模式的地方。無論在哪使用 tenEighty,你都要考慮使用 alsoTenEighty 的代碼,反之亦然。相反,值類型就更容易理解了,因為你的源碼中與同一個值交互的代碼都很近。

需要注意的是 tenEightyalsoTenEighty 被聲明為常量而不是變量。然而你依然可以改變 tenEighty.frameRatealsoTenEighty.frameRate,這是因為 tenEightyalsoTenEighty 這兩個常量的值并未改變。它們并不“存儲”這個 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)存

? 枚舉 屬性 ?
?