Swift-2.14構造過程

本頁包含內容:html

構造過程是使用類、結構體或枚舉類型的實例以前的準備過程。在新實例可用前必須執行這個過程,具體操做包括設置實例中每一個存儲型屬性的初始值和執行其餘必須的設置或初始化工做。ios

經過定義構造器(Initializers)來實現構造過程,這些構造器能夠看作是用來建立特定類型新實例的特殊方法。與 Objective-C 中的構造器不一樣,Swift 的構造器無需返回值,它們的主要任務是保證新實例在第一次使用前完成正確的初始化。swift

類的實例也能夠經過定義析構器(deinitializer)在實例釋放以前執行特定的清除工做。想了解更多關於析構器的內容,請參考析構過程數組

存儲屬性的初始賦值

類和結構體在建立實例時,必須爲全部存儲型屬性設置合適的初始值。存儲型屬性的值不能處於一個未知的狀態。安全

你能夠在構造器中爲存儲型屬性賦初值,也能夠在定義屬性時爲其設置默認值。如下小節將詳細介紹這兩種方法。閉包

注意
當你爲存儲型屬性設置默認值或者在構造器中爲其賦值時,它們的值是被直接設置的,不會觸發任何屬性觀察者(property observers)。app

構造器

構造器在建立某個特定類型的新實例時被調用。它的最簡形式相似於一個不帶任何參數的實例方法,以關鍵字init命名:ide

init() { // 在此處執行構造過程 } 

下面例子中定義了一個用來保存華氏溫度的結構體Fahrenheit,它擁有一個Double類型的存儲型屬性temperature函數

struct Fahrenheit { var temperature: Double init() { temperature = 32.0 } } var f = Fahrenheit() print("The default temperature is \(f.temperature)° Fahrenheit") // 輸出 "The default temperature is 32.0° Fahrenheit」 

這個結構體定義了一個不帶參數的構造器init,並在裏面將存儲型屬性temperature的值初始化爲32.0(華氏溫度下水的冰點)。工具

默認屬性值

如前所述,你能夠在構造器中爲存儲型屬性設置初始值。一樣,你也能夠在屬性聲明時爲其設置默認值。

注意
若是一個屬性老是使用相同的初始值,那麼爲其設置一個默認值比每次都在構造器中賦值要好。兩種方法的效果是同樣的,只不過使用默認值讓屬性的初始化和聲明結合得更緊密。使用默認值能讓你的構造器更簡潔、更清晰,且能經過默認值自動推導出屬性的類型;同時,它也能讓你充分利用默認構造器、構造器繼承等特性(後續章節將講到)。

你可使用更簡單的方式在定義結構體Fahrenheit時爲屬性temperature設置默認值:

struct Fahrenheit { var temperature = 32.0 } 

自定義構造過程

你能夠經過輸入參數和可選類型的屬性來自定義構造過程,也能夠在構造過程當中修改常量屬性。這些都將在後面章節中提到。

構造參數

自定義構造過程時,能夠在定義中提供構造參數,指定所需值的類型和名字。構造參數的功能和語法跟函數和方法的參數相同。

下面例子中定義了一個包含攝氏度溫度的結構體Celsius。它定義了兩個不一樣的構造器:init(fromFahrenheit:)init(fromKelvin:),兩者分別經過接受不一樣溫標下的溫度值來建立新的實例:

struct Celsius { var temperatureInCelsius: Double init(fromFahrenheit fahrenheit: Double) { temperatureInCelsius = (fahrenheit - 32.0) / 1.8 } init(fromKelvin kelvin: Double) { temperatureInCelsius = kelvin - 273.15 } } let boilingPointOfWater = Celsius(fromFahrenheit: 212.0) // boilingPointOfWater.temperatureInCelsius 是 100.0 let freezingPointOfWater = Celsius(fromKelvin: 273.15) // freezingPointOfWater.temperatureInCelsius 是 0.0」 

第一個構造器擁有一個構造參數,其外部名字爲fromFahrenheit,內部名字爲fahrenheit;第二個構造器也擁有一個構造參數,其外部名字爲fromKelvin,內部名字爲kelvin。這兩個構造器都將惟一的參數值轉換成攝氏溫度值,並保存在屬性temperatureInCelsius中。

參數的內部名稱和外部名稱

跟函數和方法參數相同,構造參數也擁有一個在構造器內部使用的參數名字和一個在調用構造器時使用的外部參數名字。

然而,構造器並不像函數和方法那樣在括號前有一個可辨別的名字。所以在調用構造器時,主要經過構造器中的參數名和類型來肯定應該被調用的構造器。正由於參數如此重要,若是你在定義構造器時沒有提供參數的外部名字,Swift 會爲每一個構造器的參數自動生成一個跟內部名字相同的外部名。

如下例子中定義了一個結構體Color,它包含了三個常量:redgreenblue。這些屬性能夠存儲0.01.0之間的值,用來指示顏色中紅、綠、藍成分的含量。

Color提供了一個構造器,其中包含三個Double類型的構造參數。Color也能夠提供第二個構造器,它只包含名爲whiteDouble類型的參數,它被用於給上述三個構造參數賦予一樣的值。

struct Color { let red, green, blue: Double init(red: Double, green: Double, blue: Double) { self.red = red self.green = green self.blue = blue } init(white: Double) { red = white green = white blue = white } } 

兩種構造器都能用於建立一個新的Color實例,你須要爲構造器每一個外部參數傳值:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0) let halfGray = Color(white: 0.5) 

注意,若是不經過外部參數名字傳值,你是無法調用這個構造器的。只要構造器定義了某個外部參數名,你就必須使用它,忽略它將致使編譯錯誤:

let veryGreen = Color(0.0, 1.0, 0.0) // 報編譯時錯誤,須要外部名稱 

不帶外部名的構造器參數

若是你不但願爲構造器的某個參數提供外部名字,你可使用下劃線(_)來顯式描述它的外部名,以此重寫上面所說的默認行爲。

下面是以前Celsius例子的擴展,跟以前相比添加了一個帶有Double類型參數的構造器,其外部名用_代替:

struct Celsius { var temperatureInCelsius: Double init(fromFahrenheit fahrenheit: Double) { temperatureInCelsius = (fahrenheit - 32.0) / 1.8 } init(fromKelvin kelvin: Double) { temperatureInCelsius = kelvin - 273.15 } init(_ celsius: Double){ temperatureInCelsius = celsius } } let bodyTemperature = Celsius(37.0) // bodyTemperature.temperatureInCelsius 爲 37.0 

調用Celsius(37.0)意圖明確,不須要外部參數名稱。所以適合使用init(_ celsius: Double)這樣的構造器,從而能夠經過提供Double類型的參數值調用構造器,而不須要加上外部名。

可選屬性類型

若是你定製的類型包含一個邏輯上容許取值爲空的存儲型屬性——不管是由於它沒法在初始化時賦值,仍是由於它在以後某個時間點能夠賦值爲空——你都須要將它定義爲可選類型optional type。可選類型的屬性將自動初始化爲nil,表示這個屬性是有意在初始化時設置爲空的。

下面例子中定義了類SurveyQuestion,它包含一個可選字符串屬性response

class SurveyQuestion { var text: String var response: String? init(text: String) { self.text = text } func ask() { print(text) } } let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?") cheeseQuestion.ask() // 輸出 "Do you like cheese?" cheeseQuestion.response = "Yes, I do like cheese." 

調查問題的答案在回答前是沒法肯定的,所以咱們將屬性response聲明爲String?類型,或者說是可選字符串類型optional String。當SurveyQuestion實例化時,它將自動賦值爲nil,代表此字符串暫時尚未值。

構造過程當中常量屬性的修改

你能夠在構造過程當中的任意時間點修改常量屬性的值,只要在構造過程結束時是一個肯定的值。一旦常量屬性被賦值,它將永遠不可更改。

注意
對於類的實例來講,它的常量屬性只能在定義它的類的構造過程當中修改;不能在子類中修改。

你能夠修改上面的SurveyQuestion示例,用常量屬性替代變量屬性text,表示問題內容textSurveyQuestion的實例被建立以後不會再被修改。儘管text屬性如今是常量,咱們仍然能夠在類的構造器中設置它的值:

class SurveyQuestion { let text: String var response: String? init(text: String) { self.text = text } func ask() { print(text) } } let beetsQuestion = SurveyQuestion(text: "How about beets?") beetsQuestion.ask() // 輸出 "How about beets?" beetsQuestion.response = "I also like beets. (But not with cheese.)" 

默認構造器

若是結構體或類的全部屬性都有默認值,同時沒有自定義的構造器,那麼 Swift 會給這些結構體或類提供一個默認構造器。這個默認構造器將簡單地建立一個全部屬性值都設置爲默認值的實例。

下面例子中建立了一個類ShoppingListItem,它封裝了購物清單中的某一物品的屬性:名字(name)、數量(quantity)和購買狀態 purchase state

class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem() 

因爲ShoppingListItem類中的全部屬性都有默認值,且它是沒有父類的基類,它將自動得到一個能夠爲全部屬性設置默認值的默認構造器(儘管代碼中沒有顯式爲name屬性設置默認值,但因爲name是可選字符串類型,它將默認設置爲nil)。上面例子中使用默認構造器創造了一個ShoppingListItem類的實例(使用ShoppingListItem()形式的構造器語法),並將其賦值給變量item

結構體的逐一成員構造器

除了上面提到的默認構造器,若是結構體沒有提供自定義的構造器,它們將自動得到一個逐一成員構造器,即便結構體的存儲型屬性沒有默認值。

逐一成員構造器是用來初始化結構體新實例裏成員屬性的快捷方法。咱們在調用逐一成員構造器時,經過與成員屬性名相同的參數名進行傳值來完成對成員屬性的初始賦值。

下面例子中定義了一個結構體Size,它包含兩個屬性widthheight。Swift 能夠根據這兩個屬性的初始賦值0.0自動推導出它們的類型爲Double

因爲這兩個存儲型屬性都有默認值,結構體Size自動得到了一個逐一成員構造器init(width:height:)。你能夠用它來爲Size建立新的實例:

struct Size { var width = 0.0, height = 0.0 } let twoByTwo = Size(width: 2.0, height: 2.0) 

值類型的構造器代理

構造器能夠經過調用其它構造器來完成實例的部分構造過程。這一過程稱爲構造器代理,它能減小多個構造器間的代碼重複。

構造器代理的實現規則和形式在值類型和類類型中有所不一樣。值類型(結構體和枚舉類型)不支持繼承,因此構造器代理的過程相對簡單,由於它們只能代理給提供給它的構造器。類則不一樣,它能夠繼承自其它類(請參考繼承),這意味着類有責任保證其全部繼承的存儲型屬性在構造時也能正確的初始化。這些責任將在後續章節類的繼承和構造過程中介紹。

對於值類型,你可使用self.init在自定義的構造器中引用類型中的其它構造器。而且你只能在構造器內部調用self.init

若是你爲某個值類型定義了一個自定義的構造器,你將沒法訪問到默認構造器(若是是結構體,還將沒法訪問逐一成員構造器)。這個限制能夠防止你爲值類型定義了一個進行額外必要設置的複雜構造器以後,別人仍是錯誤地使用了一個自動生成的構造器。

注意
假如你但願默認構造器、逐一成員構造器以及你本身的自定義構造器都能用來建立實例,能夠將自定義的構造器寫到擴展(extension)中,而不是寫在值類型的原始定義中。想查看更多內容,請查看擴展章節。

下面例子將定義一個結構體Rect,用來表明幾何矩形。這個例子須要兩個輔助的結構體SizePoint,它們各自爲其全部的屬性提供了初始值0.0

struct Size { var width = 0.0, height = 0.0 } struct Point { var x = 0.0, y = 0.0 } 

你能夠經過如下三種方式爲Rect建立實例——使用被初始化爲默認值的originsize屬性來初始化;提供指定的originsize實例來初始化;提供指定的centersize來初始化。在下面Rect結構體定義中,咱們爲這三種方式提供了三個自定義的構造器:

struct Rect { var origin = Point() var size = Size() init() {} init(origin: Point, size: Size) { self.origin = origin self.size = size } init(center: Point, size: Size) { let originX = center.x - (size.width / 2) let originY = center.y - (size.height / 2) self.init(origin: Point(x: originX, y: originY), size: size) } } 

第一個Rect構造器init(),在功能上跟沒有自定義構造器時自動得到的默認構造器是同樣的。這個構造器是一個空函數,使用一對大括號{}來表示,它沒有執行任何構造過程。調用這個構造器將返回一個Rect實例,它的originsize屬性都使用定義時的默認值Point(x: 0.0, y: 0.0)Size(width: 0.0, height: 0.0)

let basicRect = Rect() // basicRect 的 origin 是 (0.0, 0.0),size 是 (0.0, 0.0) 

第二個Rect構造器init(origin:size:),在功能上跟結構體在沒有自定義構造器時得到的逐一成員構造器是同樣的。這個構造器只是簡單地將originsize的參數值賦給對應的存儲型屬性:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0)) // originRect 的 origin 是 (2.0, 2.0),size 是 (5.0, 5.0) 

第三個Rect構造器init(center:size:)稍微複雜一點。它先經過centersize的值計算出origin的座標,而後再調用(或者說代理給)init(origin:size:)構造器來將新的originsize值賦值到對應的屬性中:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0)) // centerRect 的 origin 是 (2.5, 2.5),size 是 (3.0, 3.0) 

構造器init(center:size:)能夠直接將originsize的新值賦值到對應的屬性中。然而,利用剛好提供了相關功能的現有構造器會更爲方便,構造器init(center:size:)的意圖也會更加清晰。

注意
若是你想用另一種不須要本身定義init()init(origin:size:)的方式來實現這個例子,請參考擴展

類的繼承和構造過程

類裏面的全部存儲型屬性——包括全部繼承自父類的屬性——都必須在構造過程當中設置初始值。

Swift 爲類類型提供了兩種構造器來確保實例中全部存儲型屬性都能得到初始值,它們分別是指定構造器和便利構造器。

指定構造器和便利構造器

指定構造器是類中最主要的構造器。一個指定構造器將初始化類中提供的全部屬性,並根據父類鏈往上調用父類的構造器來實現父類的初始化。

每個類都必須擁有至少一個指定構造器。在某些狀況下,許多類經過繼承了父類中的指定構造器而知足了這個條件。具體內容請參考後續章節構造器的自動繼承

便利構造器是類中比較次要的、輔助型的構造器。你能夠定義便利構造器來調用同一個類中的指定構造器,併爲其參數提供默認值。你也能夠定義便利構造器來建立一個特殊用途或特定輸入值的實例。

你應當只在必要的時候爲類提供便利構造器,比方說某種狀況下經過使用便利構造器來快捷調用某個指定構造器,可以節省更多開發時間並讓類的構造過程更清晰明瞭。

指定構造器和便利構造器的語法

類的指定構造器的寫法跟值類型簡單構造器同樣:

init(parameters) { statements } 

便利構造器也採用相一樣式的寫法,但須要在init關鍵字以前放置convenience關鍵字,並使用空格將它們倆分開:

convenience init(parameters) { statements } 

類的構造器代理規則

爲了簡化指定構造器和便利構造器之間的調用關係,Swift 採用如下三條規則來限制構造器之間的代理調用:

規則 1

指定構造器必須調用其直接父類的的指定構造器。

規則 2

便利構造器必須調用同一類中定義的其它構造器。

規則 3

便利構造器必須最終致使一個指定構造器被調用。

一個更方便記憶的方法是:

  • 指定構造器必須老是向上代理
  • 便利構造器必須老是橫向代理

這些規則能夠經過下面圖例來講明:

構造器代理圖

如圖所示,父類中包含一個指定構造器和兩個便利構造器。其中一個便利構造器調用了另一個便利構造器,然後者又調用了惟一的指定構造器。這知足了上面提到的規則 2 和 3。這個父類沒有本身的父類,因此規則 1 沒有用到。

子類中包含兩個指定構造器和一個便利構造器。便利構造器必須調用兩個指定構造器中的任意一個,由於它只能調用同一個類裏的其餘構造器。這知足了上面提到的規則 2 和 3。而兩個指定構造器必須調用父類中惟一的指定構造器,這知足了規則 1。

注意
這些規則不會影響類的實例如何建立。任何上圖中展現的構造器均可以用來建立徹底初始化的實例。這些規則隻影響類定義如何實現。

下面圖例中展現了一種涉及四個類的更復雜的類層級結構。它演示了指定構造器是如何在類層級中充當「管道」的做用,在類的構造器鏈上簡化了類之間的相互關係。

複雜構造器代理圖

兩段式構造過程

Swift 中類的構造過程包含兩個階段。第一個階段,每一個存儲型屬性經過引入它們的類的構造器來設置初始值。當每個存儲型屬性值被肯定後,第二階段開始,它給每一個類一次機會在新實例準備使用以前進一步定製它們的存儲型屬性。

兩段式構造過程的使用讓構造過程更安全,同時在整個類層級結構中給予了每一個類徹底的靈活性。兩段式構造過程能夠防止屬性值在初始化以前被訪問,也能夠防止屬性被另一個構造器意外地賦予不一樣的值。

注意
Swift 的兩段式構造過程跟 Objective-C 中的構造過程相似。最主要的區別在於階段 1,Objective-C 給每個屬性賦值0或空值(好比說0nil)。Swift 的構造流程則更加靈活,它容許你設置定製的初始值,並自如應對某些屬性不能以0nil做爲合法默認值的狀況。

Swift 編譯器將執行 4 種有效的安全檢查,以確保兩段式構造過程能順利完成:

安全檢查 1

指定構造器必須保證它所在類引入的全部屬性都必須先初始化完成,以後才能將其它構造任務向上代理給父類中的構造器。

如上所述,一個對象的內存只有在其全部存儲型屬性肯定以後才能徹底初始化。爲了知足這一規則,指定構造器必須保證它所在類引入的屬性在它往上代理以前先完成初始化。

安全檢查 2

指定構造器必須先向上代理調用父類構造器,而後再爲繼承的屬性設置新值。若是沒這麼作,指定構造器賦予的新值將被父類中的構造器所覆蓋。

安全檢查 3

便利構造器必須先代理調用同一類中的其它構造器,而後再爲任意屬性賦新值。若是沒這麼作,便利構造器賦予的新值將被同一類中其它指定構造器所覆蓋。

安全檢查 4

構造器在第一階段構造完成以前,不能調用任何實例方法,不能讀取任何實例屬性的值,不能引用self做爲一個值。

類實例在第一階段結束之前並非徹底有效的。只有第一階段完成後,該實例纔會成爲有效實例,才能訪問屬性和調用方法。

如下是兩段式構造過程當中基於上述安全檢查的構造流程展現:

階段 1
  • 某個指定構造器或便利構造器被調用。
  • 完成新實例內存的分配,但此時內存尚未被初始化。
  • 指定構造器確保其所在類引入的全部存儲型屬性都已賦初值。存儲型屬性所屬的內存完成初始化。
  • 指定構造器將調用父類的構造器,完成父類屬性的初始化。
  • 這個調用父類構造器的過程沿着構造器鏈一直往上執行,直到到達構造器鏈的最頂部。
  • 當到達了構造器鏈最頂部,且已確保全部實例包含的存儲型屬性都已經賦值,這個實例的內存被認爲已經徹底初始化。此時階段 1 完成。
階段 2
  • 從頂部構造器鏈一直往下,每一個構造器鏈中類的指定構造器都有機會進一步定製實例。構造器此時能夠訪問self、修改它的屬性並調用實例方法等等。
  • 最終,任意構造器鏈中的便利構造器能夠有機會定製實例和使用self

下圖展現了在假定的子類和父類之間的構造階段 1:

構建過程階段1

在這個例子中,構造過程從對子類中一個便利構造器的調用開始。這個便利構造器此時無法修改任何屬性,它把構造任務代理給同一類中的指定構造器。

如安全檢查 1 所示,指定構造器將確保全部子類的屬性都有值。而後它將調用父類的指定構造器,並沿着構造器鏈一直往上完成父類的構造過程。

父類中的指定構造器確保全部父類的屬性都有值。因爲沒有更多的父類須要初始化,也就無需繼續向上代理。

一旦父類中全部屬性都有了初始值,實例的內存被認爲是徹底初始化,階段 1 完成。

如下展現了相同構造過程的階段 2:

構建過程階段2

父類中的指定構造器如今有機會進一步來定製實例(儘管這不是必須的)。

一旦父類中的指定構造器完成調用,子類中的指定構造器能夠執行更多的定製操做(這也不是必須的)。

最終,一旦子類的指定構造器完成調用,最開始被調用的便利構造器能夠執行更多的定製操做。

構造器的繼承和重寫

跟 Objective-C 中的子類不一樣,Swift 中的子類默認狀況下不會繼承父類的構造器。Swift 的這種機制能夠防止一個父類的簡單構造器被一個更專業的子類繼承,並被錯誤地用來建立子類的實例。

注意
父類的構造器僅會在安全和適當的狀況下被繼承。具體內容請參考後續章節構造器的自動繼承

假如你但願自定義的子類中能提供一個或多個跟父類相同的構造器,你能夠在子類中提供這些構造器的自定義實現。

當你在編寫一個和父類中指定構造器相匹配的子類構造器時,你其實是在重寫父類的這個指定構造器。所以,你必須在定義子類構造器時帶上override修飾符。即便你重寫的是系統自動提供的默認構造器,也須要帶上override修飾符,具體內容請參考默認構造器

正如重寫屬性,方法或者是下標腳本,override修飾符會讓編譯器去檢查父類中是否有相匹配的指定構造器,並驗證構造器參數是否正確。

注意
當你重寫一個父類的指定構造器時,你老是須要寫override修飾符,即便你的子類將父類的指定構造器重寫爲了便利構造器。

相反,若是你編寫了一個和父類便利構造器相匹配的子類構造器,因爲子類不能直接調用父類的便利構造器(每一個規則都在上文類的構造器代理規則有所描述),所以,嚴格意義上來說,你的子類並未對一個父類構造器提供重寫。最後的結果就是,你在子類中「重寫」一個父類便利構造器時,不須要加override前綴。

在下面的例子中定義了一個叫Vehicle的基類。基類中聲明瞭一個存儲型屬性numberOfWheels,它是值爲0Int類型的存儲型屬性。numberOfWheels屬性用於建立名爲descrpiptionString類型的計算型屬性:

class Vehicle { var numberOfWheels = 0 var description: String { return "\(numberOfWheels) wheel(s)" } } 

Vehicle類只爲存儲型屬性提供默認值,而不自定義構造器。所以,它會自動得到一個默認構造器,具體內容請參考默認構造器。自動得到的默認構造器總會是類中的指定構造器,它能夠用於建立numberOfWheels0Vehicle實例:

let vehicle = Vehicle() print("Vehicle: \(vehicle.description)") // Vehicle: 0 wheel(s) 

下面例子中定義了一個Vehicle的子類Bicycle

class Bicycle: Vehicle { override init() { super.init() numberOfWheels = 2 } } 

子類Bicycle定義了一個自定義指定構造器init()。這個指定構造器和父類的指定構造器相匹配,因此Bicycle中的指定構造器須要帶上override修飾符。

Bicycle的構造器init()以調用super.init()方法開始,這個方法的做用是調用Bicycle的父類Vehicle的默認構造器。這樣能夠確保Bicycle在修改屬性以前,它所繼承的屬性numberOfWheels能被Vehicle類初始化。在調用super.init()以後,屬性numberOfWheels的原值被新值2替換。

若是你建立一個Bicycle實例,你能夠調用繼承的description計算型屬性去查看屬性numberOfWheels是否有改變:

let bicycle = Bicycle() print("Bicycle: \(bicycle.description)") // Bicycle: 2 wheel(s) 

注意
子類能夠在初始化時修改繼承來的變量屬性,可是不能修改繼承來的常量屬性。

構造器的自動繼承

如上所述,子類在默認狀況下不會繼承父類的構造器。可是若是知足特定條件,父類構造器是能夠被自動繼承的。在實踐中,這意味着對於許多常見場景你沒必要重寫父類的構造器,而且能夠在安全的狀況下以最小的代價繼承父類的構造器。

假設你爲子類中引入的全部新屬性都提供了默認值,如下 2 個規則適用:

規則 1

若是子類沒有定義任何指定構造器,它將自動繼承全部父類的指定構造器。

規則 2

若是子類提供了全部父類指定構造器的實現——不管是經過規則 1 繼承過來的,仍是提供了自定義實現——它將自動繼承全部父類的便利構造器。(即便屬性沒有默認值,只要實現了父類的全部指定構造器,就會自動繼承父類的全部便利構造器)

即便你在子類中添加了更多的便利構造器,這兩條規則仍然適用。

注意
對於規則 2,子類能夠將父類的指定構造器實現爲便利構造器。

指定構造器和便利構造器實踐

接下來的例子將在實踐中展現指定構造器、便利構造器以及構造器的自動繼承。這個例子定義了包含三個類FoodRecipeIngredient以及ShoppingListItem的類層次結構,並將演示它們的構造器是如何相互做用的。

類層次中的基類是Food,它是一個簡單的用來封裝食物名字的類。Food類引入了一個叫作nameString類型的屬性,而且提供了兩個構造器來建立Food實例:

class Food { var name: String init(name: String) { self.name = name } convenience init() { self.init(name: "[Unnamed]") } } 

下圖中展現了Food的構造器鏈:

Food構造器鏈

類類型沒有默認的逐一成員構造器,因此Food類提供了一個接受單一參數name的指定構造器。這個構造器可使用一個特定的名字來建立新的Food實例:

let namedMeat = Food(name: "Bacon") // namedMeat 的名字是 "Bacon」 

Food類中的構造器init(name: String)被定義爲一個指定構造器,由於它能確保Food實例的全部存儲型屬性都被初始化。Food類沒有父類,因此init(name: String)構造器不須要調用super.init()來完成構造過程。

Food類一樣提供了一個沒有參數的便利構造器init()。這個init()構造器爲新食物提供了一個默認的佔位名字,經過橫向代理到指定構造器init(name: String)並給參數name傳值[Unnamed]來實現:

let mysteryMeat = Food() // mysteryMeat 的名字是 [Unnamed] 

類層級中的第二個類是Food的子類RecipeIngredientRecipeIngredient類構建了食譜中的一味調味劑。它引入了Int類型的屬性quantity(以及從Food繼承過來的name屬性),而且定義了兩個構造器來建立RecipeIngredient實例:

class RecipeIngredient: Food { var quantity: Int init(name: String, quantity: Int) { self.quantity = quantity super.init(name: name) } override convenience init(name: String) { self.init(name: name, quantity: 1) } } 

下圖中展現了RecipeIngredient類的構造器鏈:

RecipeIngredient構造器

RecipeIngredient類擁有一個指定構造器init(name: String, quantity: Int),它能夠用來填充RecipeIngredient實例的全部屬性值。這個構造器一開始先將傳入的quantity參數賦值給quantity屬性,這個屬性也是惟一在RecipeIngredient中新引入的屬性。隨後,構造器向上代理到父類Foodinit(name: String)。這個過程知足兩段式構造過程中的安全檢查 1。

RecipeIngredient還定義了一個便利構造器init(name: String),它只經過name來建立RecipeIngredient的實例。這個便利構造器假設任意RecipeIngredient實例的quantity1,因此不須要顯式指明數量便可建立出實例。這個便利構造器的定義能夠更加方便和快捷地建立實例,而且避免了建立多個quantity1RecipeIngredient實例時的代碼重複。這個便利構造器只是簡單地橫向代理到類中的指定構造器,併爲quantity參數傳遞1

注意,RecipeIngredient的便利構造器init(name: String)使用了跟Food中指定構造器init(name: String)相同的參數。因爲這個便利構造器重寫了父類的指定構造器init(name: String),所以必須在前面使用override修飾符(參見構造器的繼承和重寫)。

儘管RecipeIngredient將父類的指定構造器重寫爲了便利構造器,它依然提供了父類的全部指定構造器的實現。所以,RecipeIngredient會自動繼承父類的全部便利構造器。

在這個例子中,RecipeIngredient的父類是Food,它有一個便利構造器init()。這個便利構造器會被RecipeIngredient繼承。這個繼承版本的init()在功能上跟Food提供的版本是同樣的,只是它會代理到RecipeIngredient版本的init(name: String)而不是Food提供的版本。

全部的這三種構造器均可以用來建立新的RecipeIngredient實例:

let oneMysteryItem = RecipeIngredient() let oneBacon = RecipeIngredient(name: "Bacon") let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6) 

類層級中第三個也是最後一個類是RecipeIngredient的子類,叫作ShoppingListItem。這個類構建了購物單中出現的某一種調味料。

購物單中的每一項老是從未購買狀態開始的。爲了呈現這一事實,ShoppingListItem引入了一個布爾類型的屬性purchased,它的默認值是falseShoppingListItem還添加了一個計算型屬性description,它提供了關於ShoppingListItem實例的一些文字描述:

class ShoppingListItem: RecipeIngredient { var purchased = false var description: String { var output = "\(quantity) x \(name)" output += purchased ? " ✔" : " ✘" return output } } 

注意
ShoppingListItem沒有定義構造器來爲purchased提供初始值,由於添加到購物單的物品的初始狀態老是未購買。

因爲它爲本身引入的全部屬性都提供了默認值,而且本身沒有定義任何構造器,ShoppingListItem將自動繼承全部父類中的指定構造器和便利構造器。

下圖展現了這三個類的構造器鏈:

三類構造器圖

你可使用所有三個繼承來的構造器來建立ShoppingListItem的新實例:

var breakfastList = [ ShoppingListItem(), ShoppingListItem(name: "Bacon"), ShoppingListItem(name: "Eggs", quantity: 6), ] breakfastList[0].name = "Orange juice" breakfastList[0].purchased = true for item in breakfastList { print(item.description) } // 1 x orange juice ✔ // 1 x bacon ✘ // 6 x eggs ✘ 

如上所述,例子中經過字面量方式建立了一個數組breakfastList,它包含了三個ShoppingListItem實例,所以數組的類型也能被自動推導爲[ShoppingListItem]。在數組建立完以後,數組中第一個ShoppingListItem實例的名字從[Unnamed]更改成Orange juice,並標記爲已購買。打印數組中每一個元素的描述顯示了它們都已按照預期被賦值。

可失敗構造器

若是一個類、結構體或枚舉類型的對象,在構造過程當中有可能失敗,則爲其定義一個可失敗構造器。這裏所指的「失敗」是指,如給構造器傳入無效的參數值,或缺乏某種所需的外部資源,又或是不知足某種必要的條件等。

爲了妥善處理這種構造過程當中可能會失敗的狀況。你能夠在一個類,結構體或是枚舉類型的定義中,添加一個或多個可失敗構造器。其語法爲在init關鍵字後面加添問號(init?)

注意
可失敗構造器的參數名和參數類型,不能與其它非可失敗構造器的參數名,及其參數類型相同。

可失敗構造器會建立一個類型爲自身類型的可選類型的對象。你經過return nil語句來代表可失敗構造器在何種狀況下應該「失敗」。

注意
嚴格來講,構造器都不支持返回值。由於構造器自己的做用,只是爲了確保對象能被正確構造。所以你只是用return nil代表可失敗構造器構造失敗,而不要用關鍵字return來代表構形成功。

下例中,定義了一個名爲Animal的結構體,其中有一個名爲speciesString類型的常量屬性。同時該結構體還定義了一個接受一個名爲speciesString類型參數的可失敗構造器。這個可失敗構造器檢查傳入的參數是否爲一個空字符串。若是爲空字符串,則構造失敗。不然,species屬性被賦值,構形成功。

struct Animal { let species: String init?(species: String) { if species.isEmpty { return nil } self.species = species } } 

你能夠經過該可失敗構造器來構建一個Animal的實例,並檢查構造過程是否成功:

let someCreature = Animal(species: "Giraffe") // someCreature 的類型是 Animal? 而不是 Animal if let giraffe = someCreature { print("An animal was initialized with a species of \(giraffe.species)") } // 打印 "An animal was initialized with a species of Giraffe" 

若是你給該可失敗構造器傳入一個空字符串做爲其參數,則會致使構造失敗:

let anonymousCreature = Animal(species: "") // anonymousCreature 的類型是 Animal?, 而不是 Animal if anonymousCreature == nil { print("The anonymous creature could not be initialized") } // 打印 "The anonymous creature could not be initialized" 

注意
空字符串(如"",而不是"Giraffe")和一個值爲nil的可選類型的字符串是兩個徹底不一樣的概念。上例中的空字符串("")實際上是一個有效的,非可選類型的字符串。這裏咱們之因此讓Animal的可失敗構造器構造失敗,只是由於對於Animal這個類的species屬性來講,它更適合有一個具體的值,而不是空字符串。

枚舉類型的可失敗構造器

你能夠經過一個帶一個或多個參數的可失敗構造器來獲取枚舉類型中特定的枚舉成員。若是提供的參數沒法匹配任何枚舉成員,則構造失敗。

下例中,定義了一個名爲TemperatureUnit的枚舉類型。其中包含了三個可能的枚舉成員(KelvinCelsius,和Fahrenheit),以及一個根據Character值找出所對應的枚舉成員的可失敗構造器:

enum TemperatureUnit { case Kelvin, Celsius, Fahrenheit init?(symbol: Character) { switch symbol { case "K": self = .Kelvin case "C": self = .Celsius case "F": self = .Fahrenheit default: return nil } } } 

你能夠利用該可失敗構造器在三個枚舉成員中獲取一個相匹配的枚舉成員,當參數的值不能與任何枚舉成員相匹配時,則構造失敗:

let fahrenheitUnit = TemperatureUnit(symbol: "F") if fahrenheitUnit != nil { print("This is a defined temperature unit, so initialization succeeded.") } // 打印 "This is a defined temperature unit, so initialization succeeded." let unknownUnit = TemperatureUnit(symbol: "X") if unknownUnit == nil { print("This is not a defined temperature unit, so initialization failed.") } // 打印 "This is not a defined temperature unit, so initialization failed." 

帶原始值的枚舉類型的可失敗構造器

帶原始值的枚舉類型會自帶一個可失敗構造器init?(rawValue:),該可失敗構造器有一個名爲rawValue的參數,其類型和枚舉類型的原始值類型一致,若是該參數的值可以和某個枚舉成員的原始值匹配,則該構造器會構造相應的枚舉成員,不然構造失敗。

所以上面的TemperatureUnit的例子能夠重寫爲:

enum TemperatureUnit: Character { case Kelvin = "K", Celsius = "C", Fahrenheit = "F" } let fahrenheitUnit = TemperatureUnit(rawValue: "F") if fahrenheitUnit != nil { print("This is a defined temperature unit, so initialization succeeded.") } // 打印 "This is a defined temperature unit, so initialization succeeded." let unknownUnit = TemperatureUnit(rawValue: "X") if unknownUnit == nil { print("This is not a defined temperature unit, so initialization failed.") } // 打印 "This is not a defined temperature unit, so initialization failed." 

類的可失敗構造器

值類型(也就是結構體或枚舉)的可失敗構造器,能夠在構造過程當中的任意時間點觸發構造失敗。好比在前面的例子中,結構體Animal的可失敗構造器在構造過程一開始就觸發了構造失敗,甚至在species屬性被初始化前。

而對類而言,可失敗構造器只能在類引入的全部存儲型屬性被初始化後,以及構造器代理調用完成後,才能觸發構造失敗。

下面例子展現瞭如何在類的可失敗構造器中使用隱式解包可選類型來知足上述要求:

class Product { let name: String! init?(name: String) { self.name = name if name.isEmpty { return nil } } } 

上面定義的Product類和以前的Animal結構體很類似。Product類有一個不能爲空字符串的常量屬性name。爲了強制這個要求,Product類使用了可失敗構造器確保這個屬性的值不是空字符串後,才容許構形成功。

畢竟,Product是一個類而不是結構體,這意味着不一樣於AnimalProduct類的全部可失敗構造器必須給name屬性一個初始值,而後才能觸發構造失敗。

上面的例子中,Product類的name屬性被定義爲隱式解包可選字符串類型(String!)。由於它是一個可選類型,因此它在構造過程當中被賦值前,具備默認值nil。這個默認值nil意味着Product類引入的全部存儲型屬性都有一個有效的初始值。所以,一旦傳入一個空字符串,該可失敗構造器能夠在name屬性被賦值前觸發構造失敗。

譯者注
上面的示例代碼和描述並不相符,根據描述,if name.isEmpty { return nil }這句代碼應該在self.name = name以前,而這卻會致使編譯錯誤error: all stored properties of a class instance must be initialized before returning nil from an initializer,除非將let name: String!改成var name: String!

由於name屬性是一個常量,因此一旦構形成功,name屬性確定有一個非nil的值。即便它被定義爲隱式解包可選類型,也徹底能夠放心大膽地直接訪問,而不用檢查name屬性的值是否爲nil

if let bowTie = Product(name: "bow tie") { // 不須要檢查 bowTie.name 是否爲 nil print("The product's name is \(bowTie.name)") } // 打印 "The product's name is bow tie" 

構造失敗的傳遞

類,結構體,枚舉的可失敗構造器能夠橫向代理到類型中的其餘可失敗構造器。相似的,子類的可失敗構造器也能向上代理到父類的可失敗構造器。

不管是向上代理仍是橫向代理,若是你代理到的其餘可失敗構造器觸發構造失敗,整個構造過程將當即終止,接下來的任何構造代碼不會再被執行。

注意
可失敗構造器也能夠代理到其它的非可失敗構造器。經過這種方式,你能夠增長一個可能的失敗狀態到現有的構造過程當中。

下面這個例子,定義了一個名爲CartItemProduct類的子類。這個類創建了一個在線購物車中的物品的模型,它有一個名爲quantity的常量存儲型屬性,並確保該屬性的值至少爲1

class CartItem: Product { let quantity: Int! init?(name: String, quantity: Int) { self.quantity = quantity super.init(name: name) if quantity < 1 { return nil } } } 

Product類中的name屬性相似,CartItem類中的quantity屬性也是隱式解包可選類型。這意味着在構造過程當中,該屬性在被賦予特定的值以前能有一個默認的初始值nil

該可失敗構造器以向上代理到父類的可失敗構造器init(name:)開始。這知足了可失敗構造器在觸發構造失敗前必須老是完成構造器代理調用這個條件。

若是因爲name的值爲空字符串而致使父類的可失敗構造器構造失敗,則CartIem類的整個構造過程都將當即失敗,以後的構造代碼將不會再被執行。若是父類構形成功,CartIem的可失敗構造器會進一步驗證quantity的值是否不小於1

譯者注
上面的示例代碼和描述也不相符,根據描述,self.quantity = quantity這句代碼應該放在最後一行,而這卻會致使編譯錯誤error: property 'self.quantity' not initialized at super.init call,除非將let quantity: Int!改成var quantity: Int!

若是你構造一個name的值爲非空字符串,quantity的值不小於1CartItem實例,則可成功構造:

if let twoSocks = CartItem(name: "sock", quantity: 2) { print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)") } // 打印 "Item: sock, quantity: 2" 

若是你試圖構造一個quantity的值爲0CartItem實例, 則CartItem的可失敗構造器會觸發構造失敗:

if let zeroShirts = CartItem(name: "shirt", quantity: 0) { print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)") } else { print("Unable to initialize zero shirts") } // 打印 "Unable to initialize zero shirts" 

相似的,若是你試圖構造一個name的值爲空字符串的CartItem實例,則父類Product的可失敗構造器會觸發構造失敗:

if let oneUnnamed = CartItem(name: "", quantity: 1) { print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)") } else { print("Unable to initialize one unnamed product") } // 打印 "Unable to initialize one unnamed product" 

重寫一個可失敗構造器

如同其它的構造器,你能夠在子類中重寫父類的可失敗構造器。或者你也能夠用子類的非可失敗構造器重寫一個父類的可失敗構造器。這使你能夠定義一個不會構造失敗的子類,即便父類的構造器容許構造失敗。

注意,當你用子類的非可失敗構造器重寫父類的可失敗構造器時,向上代理到父類的可失敗構造器的惟一方式是對父類的可失敗構造器的返回值進行強制解包。

注意
你能夠用非可失敗構造器重寫可失敗構造器,但反過來卻不行。

下例定義了一個名爲Document的類,name屬性的值必須爲一個非空字符串或nil,但不能是一個空字符串:

class Document { var name: String? // 該構造器建立了一個 name 屬性的值爲 nil 的 document 實例 init() {} // 該構造器建立了一個 name 屬性的值爲非空字符串的 document 實例 init?(name: String) { self.name = name if name.isEmpty { return nil } } } 

下面這個例子,定義了一個Document類的子類AutomaticallyNamedDocument。這個子類重寫了父類的兩個指定構造器,確保了不管是使用init()構造器,仍是使用init(name:)構造器併爲參數傳遞空字符串,生成的實例中的name屬性總有初始"[Untitled]"

class AutomaticallyNamedDocument: Document { override init() { super.init() self.name = "[Untitled]" } override init(name: String) { super.init() if name.isEmpty { self.name = "[Untitled]" } else { self.name = name } } } 

AutomaticallyNamedDocument用一個非可失敗構造器init(name:)重寫了父類的可失敗構造器init?(name:)。由於子類用另外一種方式處理了空字符串的狀況,因此再也不須要一個可失敗構造器,所以子類用一個非可失敗構造器代替了父類的可失敗構造器。

你能夠在子類的非可失敗構造器中使用強制解包來調用父類的可失敗構造器。好比,下面的UntitledDocument子類的name屬性的值老是"[Untitled]",它在構造過程當中使用了父類的可失敗構造器init?(name:)

class UntitledDocument: Document { override init() { super.init(name: "[Untitled]")! } } 

在這個例子中,若是在調用父類的可失敗構造器init?(name:)時傳入的是空字符串,那麼強制解包操做會引起運行時錯誤。不過,由於這裏是經過非空的字符串常量來調用它,因此並不會發生運行時錯誤。

可失敗構造器 init!

一般來講咱們經過在init關鍵字後添加問號的方式(init?)來定義一個可失敗構造器,但你也能夠經過在init後面添加驚歎號的方式來定義一個可失敗構造器((init!)),該可失敗構造器將會構建一個對應類型的隱式解包可選類型的對象。

你能夠在init?中代理到init!,反之亦然。你也能夠用init?重寫init!,反之亦然。你還能夠用init代理到init!,不過,一旦init!構造失敗,則會觸發一個斷言。

必要構造器

在類的構造器前添加required修飾符代表全部該類的子類都必須實現該構造器:

class SomeClass { required init() { // 構造器的實現代碼 } } 

在子類重寫父類的必要構造器時,必須在子類的構造器前也添加required修飾符,代表該構造器要求也應用於繼承鏈後面的子類。在重寫父類中必要的指定構造器時,不須要添加override修飾符:

class SomeSubclass: SomeClass { required init() { // 構造器的實現代碼 } } 

注意
若是子類繼承的構造器能知足必要構造器的要求,則無須在子類中顯式提供必要構造器的實現。

經過閉包或函數設置屬性的默認值

若是某個存儲型屬性的默認值須要一些定製或設置,你可使用閉包或全局函數爲其提供定製的默認值。每當某個屬性所在類型的新實例被建立時,對應的閉包或函數會被調用,而它們的返回值會當作默認值賦值給這個屬性。

這種類型的閉包或函數一般會建立一個跟屬性類型相同的臨時變量,而後修改它的值以知足預期的初始狀態,最後返回這個臨時變量,做爲屬性的默認值。

下面介紹瞭如何用閉包爲屬性提供默認值:

class SomeClass { let someProperty: SomeType = { // 在這個閉包中給 someProperty 建立一個默認值 // someValue 必須和 SomeType 類型相同 return someValue }() } 

注意閉包結尾的大括號後面接了一對空的小括號。這用來告訴 Swift 當即執行此閉包。若是你忽略了這對括號,至關於將閉包自己做爲值賦值給了屬性,而不是將閉包的返回值賦值給屬性。

注意
若是你使用閉包來初始化屬性,請記住在閉包執行時,實例的其它部分都尚未初始化。這意味着你不能在閉包裏訪問其它屬性,即便這些屬性有默認值。一樣,你也不能使用隱式的self屬性,或者調用任何實例方法。

下面例子中定義了一個結構體Checkerboard,它構建了西洋跳棋遊戲的棋盤:

西洋跳棋棋盤

西洋跳棋遊戲在一副黑白格交替的10x10的棋盤中進行。爲了呈現這副遊戲棋盤,Checkerboard結構體定義了一個屬性boardColors,它是一個包含100Bool值的數組。在數組中,值爲true的元素表示一個黑格,值爲false的元素表示一個白格。數組中第一個元素表明棋盤上左上角的格子,最後一個元素表明棋盤上右下角的格子。

boardColor數組是經過一個閉包來初始化並設置顏色值的:

struct Checkerboard { let boardColors: [Bool] = { var temporaryBoard = [Bool]() var isBlack = false for i in 1...10 { for j in 1...10 { temporaryBoard.append(isBlack) isBlack = !isBlack } isBlack = !isBlack } return temporaryBoard }() func squareIsBlackAtRow(row: Int, column: Int) -> Bool { return boardColors[(row * 10) + column] } } 

每當一個新的Checkerboard實例被建立時,賦值閉包會被執行,boardColors的默認值會被計算出來並返回。上面例子中描述的閉包將計算出棋盤中每一個格子對應的顏色,並將這些值保存到一個臨時數組temporaryBoard中,最後在構建完成時將此數組做爲閉包返回值返回。這個返回的數組會保存到boardColors中,並能夠經過工具函數squareIsBlackAtRow來查詢:

let board = Checkerboard() print(board.squareIsBlackAtRow(0, column: 1)) // 打印 "true" print(board.squareIsBlackAtRow(9, column: 9)) // 打印 "false"
相關文章
相關標籤/搜索