讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.1 對像類型聲明與特性 >

4.1 對像類型聲明與特性

對像類型是通過一種對像類型風格(enum、struct與class)、對像類型的名字(應該以一個大寫字母開頭)和一對花括號進行聲明的:


class Manny {
}
struct Moe {
}
enum Jack {
}  

對像類型聲明可以出現在任何地方:在文件頂部、在另一個對像類型聲明頂部,或在函數體中。對像類型相對於其他代碼的可見性(作用域)與可用性取決於它聲明的位置(參見第1章):

·在默認情況下,聲明在文件頂部的對象類型對於項目(模塊)中的所有文件都可見,對像類型通常都會聲明在這個地方。

·有時需要在其他類型的聲明中聲明一個類型,從而賦予它一個命名空間,這叫作嵌套類型。

·聲明在函數體中的對象類型只會在外圍花括號的作用域內存活;這種聲明是合法的,但並不常見。

任何對像類型聲明的花括號中都可能包含如下內容:

初始化器

對像類型僅僅是一個對象的類型而已。聲明對像類型的目的通常是(但不總是這樣)創建該類型的實際對象,即實例。初始化器是個特殊的函數,它的聲明和調用方式都與眾不同,你可以通過它創建對象。

屬性

聲明在對像類型聲明頂部的變量就是屬性。在默認情況下,它是個實例屬性。實例屬性的作用域是實例:可以通過該類型的特定實例來訪問它,該類型的每個實例的實例屬性值可能都不同。

此外,屬性還可以是靜態/類屬性。對於枚舉或結構體來說,它是通過關鍵字static聲明的;對於類來說,它是通過關鍵字class聲明的。這種屬性屬於對像類型本身;可以通過類型來訪問它,它只有一個值,與所屬類型關聯。

方法

聲明在對像類型聲明頂部的函數就是方法。在默認情況下,方法都是實例方法;可以通過向該類型的特定實例發送消息來調用它。在實例方法內部,self就是實例本身。

此外,函數還可以是靜態/類方法。對於枚舉或結構體來說,它是通過關鍵字static聲明的;對於類來說,它是通過關鍵字class聲明的。可以通過向類型發送消息來調用它。在靜態/類方法內部,self就是類型。

下標

下標是一種特殊類型的實例方法,可以通過向實例引用附加方括號來調用它。

對像類型聲明

對像類型聲明還可以包含對像類型聲明,即嵌套類型。從外部對像類型內部看,嵌套類型位於其作用域中;從外部對像類型外部看,嵌套類型必須要通過外部對像類型才能使用。這樣,外部對像類型是嵌套類型的命名空間。

4.1.1 初始化器

初始化器是一個函數,用來生成對像類型的一個實例。嚴格來說,它是個靜態/類方法,因為它是通過對像類型調用的。調用時通常會使用特殊的語法:類型名後面直接跟著一對圓括號,就好像類型本身是函數一樣。當調用初始化器時,新的實例會被創建出來並作為結果返回。你通常會用到返回的實例,比如,將其賦給變量,從而將其保存起來並在後續代碼中使用它。

比如,假設有一個Dog類:


class Dog {
}  

接下來可以創建一個Dog實例:


Dog  

上述代碼雖然合法,但卻沒什麼用,甚至連編譯器都會發出警告。我們創建了一個Dog實例,但卻沒有引用該實例。如果沒有引用,那麼Dog實例創建出來後立刻就會消亡。一般來說,我們會這麼做:


let fido = Dog  

現在,只要變量fido存在,Dog實例就會存在(參見第3章),變量fido引用了Dog實例,這樣就可以使用它了。

注意,雖然Dog類沒有聲明任何初始化器,但Dog()還是調用了一個初始化器!原因在於對像類型會有隱式初始化器。這樣你就不必費力編寫自定義的初始化器了。不過,你還是可以編寫自定義的初始化器,而且會經常這麼做。

初始化器是一種函數,其聲明語法與函數非常像。要想聲明初始化器,你需要使用關鍵字init,後跟一個參數列表,然後是包含代碼的花括號。一個對像類型可以有多個初始化器,由參數進行區分。在默認情況下,參數名(包括第一個參數)都是外化的(當然了,你可以在參數名前通過下劃線阻止這一點)。參數常常用於設置實例屬性的值。

比如,下面是擁有兩個實例屬性的Dog類:name(String)與license(Int)。我們為這些實例屬性賦予了默認值,這些默認值起到了佔位符的作用——一個空字符串和一個數字0。接下來聲明了3個初始化器,這樣調用者就可以通過3種不同方式來創建Dog實例了:提供一個名字、提供一個登記號,或提供這二者。在每個初始化器中,所提供的參數都用於設置相應屬性的值:


class Dog {
    var name = ""
    var license = 0
    init(name:String) {
        self.name = name
    }
    init(license:Int) {
        self.license = license
    }
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
}  

注意,在上述代碼的每個初始化器中,我為每個參數起了與其相應的屬性相同的名字,這麼做只是一種編程風格而已。在每個初始化器中,我可以通過self訪問屬性將參數與屬性區分開。

上述聲明的結果就是我可以通過3種不同方式來創建Dog:


let fido = Dog(name:"Fido")
let rover = Dog(license:1234)
let spot = Dog(name:"Spot", license:1357)  

我無法做的是不使用初始化器參數創建Dog實例。我編寫了初始化器,因此隱式初始化器就不復存在了。如下代碼是不合法的:


let puff = Dog // compile error  

當然,可以顯式聲明一個不帶參數的初始化器,這樣上述代碼就合法了:


class Dog {
    var name = ""
    var license = 0
    init {
    }
    init(name:String) {
        self.name = name
    }
    init(license:Int) {
        self.license = license
    }
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
}  

其實不需要這麼多初始化器,因為初始化器是個函數,函數的參數可以有默認值。這樣,我可以將所有代碼放到單個初始化器中,如下代碼所示:


class Dog {
    var name = ""
    var license = 0
    init(name:String = "", license:Int = 0) {
        self.name = name
        self.license = license
    }
}  

現在依然可以通過4種不同的方式創建一個Dog實例:


let fido = Dog(name:"Fido")
let rover = Dog(license:1234)
let spot = Dog(name:"Spot", license:1357)
let puff = Dog  

現在來看看有趣的地方。在屬性聲明中,我可以去掉默認初始值的賦值(只要顯式聲明每個屬性的類型即可):


class Dog {
    var name : String // no default value!
    var license : Int // no default value!
    init(name:String = "", license:Int = 0) {
        self.name = name
        self.license = license
    }
}  

上述代碼是合法的,也很常見,因為初始化器執行的確實是初始化工作!換言之,我無須在聲明中為屬性賦初值,而是在所有的初始化器中為它們賦初值。通過這種方式,我可以保證當實例創建出來後,所有實例屬性都有值了,這正是重要之處。相反,當實例創建出來後,沒有初值的實例屬性是不合法的。屬性要麼在聲明中初始化,要麼被每個初始化器初始化,否則編譯器會報錯。

Swift編譯器認為所有實例屬性都要被恰當初始化是Swift的一個重要特性(這與Objective-C相反,它的實例屬性可以沒有初始化,這常常會導致後續一些奇怪的錯誤)。不要挑戰編譯器,請適應它。編譯器會通過錯誤消息(「Return from initializer without initializing all stored properties'」)幫助你,直到初始化器初始化了所有實例屬性。


class Dog {
    var name : String
    var license : Int
    init(name:String = "") {
        self.name = name // compile error
    }
}  

由於在初始化器中設置實例屬性算是初始化,所以即便實例屬性是通過let聲明的常量也是合法的:


class Dog {
    let name : String
    let license : Int
    init(name:String = "", license:Int = 0) {
        self.name = name
        self.license = license
    }
}  

在這個示例中,我們沒有對初始化器做任何限制:調用者可以在不提供name或license實參的情況下實例化Dog。但通常,初始化器的目的正好相反:我們會強制調用者在實例化時提供所有必要的信息。在實際情況下,Dog類更可能像是下面這樣:


class Dog {
    let name : String
    let license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
}  

在上述代碼中,Dog有一個name和一個license,這兩個變量的值是在實例化時提供的(它們沒有默認值),並且之後這兩個值就無法再改變了(這些屬性是常量)。通過這種方式,我們強制要求每個Dog都必須要有一個有意義的名字與許可證號。現在,創建Dog只有一種方式:


let spot = Dog(name:"Spot", license:1357)  

1.Optional屬性

有時,在初始化時並沒有可賦給實例屬性的有意義的默認值。比如,也許直到實例創建出來一段時間後才能獲取到屬性的初始值。這種情況與所有實例屬性要麼在聲明中,要麼通過初始化器進行初始化的要求相衝突。當然,你可以通過給實例屬性賦一個默認初始值來繞過這個問題,不過它並非「真正的」值。

正如我在第3章所提及的,這個問題合理且常見的解決方案是使用var將實例屬性聲明為Optional類型。值為nil的Optional表示沒有提供「真正的」值;Optional var會被自動初始化為nil。這樣,代碼就可以比較該實例屬性與nil,如果為nil,那就不使用該屬性。稍後,屬性會被賦予「真正的」值。當然,這個值現在被包裝到了一個Optional中;但如果將其聲明為隱式展開Optional,那麼你還可以直接使用被包裝的值,無須顯式將其展開(就好像它根本就不是Optional一樣),如果確定,那就可以這樣做:


// this property will be set automatically when the nib loads
@IBOutlet var myButton: UIButton!
// this property will be set after time-consuming gathering of data
var albums : [MPMediaItemCollection]!  

2.引用self

除了設置實例屬性,初始化器不能引用self,無論顯式還是隱式都不可以,除非所有實例屬性都完成了初始化。這個原則可以確保實例在使用前已經完全構建完畢。比如,如下代碼是不合法的:


struct Cat {
    var name : String
    var license : Int
    init(name:String, license:Int) {
        self.name = name
        meow // too soon - compile error
        self.license = license
    }
    func meow {
        print("meow")
    }
}  

對實例方法meow的調用隱式引用了self,它表示self.meow()。初始化器可以這麼做,但需要在初始化完所有未初始化的屬性後才可以。對實例方法meow的調用只需要下移一行即可,這樣在完成了name與license的初始化後就可以調用它了。

3.委託初始化器

對像類型中的初始化器可以通過語法self.init(...)調用其他初始化器。調用其他初始化器的初始化器叫作委託初始化器。當一個初始化器委託另一個初始化器時,被委託的初始化器必須要先完成實例的初始化,接下來委託初始化器才能使用初始化完畢的實例,可以再次設置被委託初始化器已經設定的var屬性。

委託初始化器看起來好像是之前介紹的關於self的規則的一個例外。但實際上並非如此,因為它要通過self才能委託,而且委託會導致所有實例屬性都被初始化。事實上,關於委託初始化器使用self的規則要更加嚴格:委託初始化器不能引用self,也不能設置屬性,直到對其他初始化器的調用完畢後才可以。比如:


struct Digit {
    var number : Int
    var meaningOfLife : Bool
    init(number:Int) {
        self.number = number
        self.meaningOfLife = false
    }
    init { // this is a delegating initializer
        self.init(number:42)
        self.meaningOfLife = true
    }
}  

此外,委託初始化器不能設置不可變屬性(即let變量)。這是因為只有在調用了其他初始化器後它才可以引用屬性,而這時實例已經構建完畢——初始化已經結束,通往不可變屬性的初始化之門已經關閉。這樣,如果meaningOfLife是通過let聲明的,那麼上述代碼就不合法,因為第2個初始化器是委託初始化器,它無法設置不可變屬性。

請注意,不要遞歸委託!如果讓初始化器委託給自身,或是創建了循環委託初始化器,那麼編譯器不會報錯(我認為這是個Bug),不過運行著的應用會掛起。比如,不要這麼做:


struct Digit { // do not do this!
    var number : Int = 100
    init(value:Int) {
        self.init(number:value)
    }
    init(number:Int) {
        self.init(value:number)
    }
}  

4.可失敗初始化器

初始化器可以返回一個包裝新實例的Optional。通過這種方式,可以返回nil來表示失敗。具備這種行為的初始化器叫作可失敗初始化器。在聲明時要想將某個初始化器標記為可失敗的,請在關鍵字init後面放置一個問號(對於隱式展開Optional,放置一個感歎號)。如果可失敗初始化器需要返回nil,請顯式寫明return nil。判斷返回的Optional與nil是否相等是調用者的事,請展開它,然後比較,與其他Optional的做法一樣。

下面這個版本的Dog有一個返回隱式展開Optional的初始化器,如果name:或是license:實參無效,那麼它會返回nil:


class Dog {
    let name : String
    let license : Int
    init!(name:String, license:Int) {
        self.name = name
        self.license = license
        if name.isEmpty {
            return nil
        }
        if license <= 0 {
            return nil
        }
    }
}  

返回值的類型是Dog,Optional會隱式展開,因此以這種方式實例化Dog的調用者可以直接使用該結果,就好像它是個Dog實例一樣。不過如果返回的是nil,那麼調用者訪問Dog實例的成員就會導致程序在運行時崩潰:


let fido = Dog(name:"", license:0)
let name = fido.name // crash  

按照慣例,Cocoa與Objective-C會從初始化器中返回nil來表示失敗;如果初始化真的可能失敗,那麼這種初始化器API已經被轉換為了Swift可失敗初始化器。比如,UIImage初始化器init?(named:)就是個可失敗初始化器,因為給定的名字可能並不表示一張圖片。它不會隱式展開,因此結果值是一個UIImage?,並且在使用前需要展開(不過,大多數Objective-C初始化器都沒有被橋接為可失敗初始化器,即便從理論上說,任何Objective-C初始化器都可能返回nil)。

4.1.2 屬性

屬性是個變量,它聲明在對像類型聲明的頂部。這意味著第3章所介紹的關於變量的一切都適用於屬性。屬性擁有確定的類型;可以通過var或let聲明屬性,它可以是存儲變量,也可以是計算變量;它也可以擁有Setter觀察者。實例屬性也可以聲明為lazy。

存儲實例屬性必須要賦予一個初始值。不過,正如我之前說過的,這不一定非得通過聲明中的賦值來實現;也可以通過初始化器。Setter觀察者在屬性的初始化過程中是不會被調用的。

初始化屬性的代碼不能獲取實例屬性,也不能調用實例方法。這種行為需要一個對self的顯式或隱式引用;在初始化過程中還不存在self,self是在初始化過程中所創建的。這個錯誤所導致的Swift編譯錯誤消息令人感到很費解。比如,如下代碼是不合法的(刪除對self的顯式引用也不行):


class Moi {
    let first = "Matt"
    let last = "Neuburg"
    let whole = self.first + " " + self.last // compile error
}  

一種解決辦法就是將whole作為一個計算屬性:


class Moi {
    let first = "Matt"
    let last = "Neuburg"
    var whole : String {
        return self.first + " " + self.last
    }
}  

這是合法的,因為計算直到self存在後才會執行。另一個解決辦法是將whole聲明為lazy:


class Moi {
    let first = "Matt"
    let last = "Neuburg"
    lazy var whole : String = self.first + " " + self.last
}  

這也是合法的,因為直到self存在後對它的引用才會執行。與之類似,屬性初始化器是無法調用實例方法的,不過,計算屬性卻可以,lazy屬性也可以。

正如第3章所述,變量的初始化器可以包含多行代碼,前提是將其寫成定義與調用匿名函數。如果變量是實例屬性,並且代碼引用了其他的實例屬性或實例方法,那麼變量就可以聲明為lazy:


class Moi {
    let first = "Matt"
    let last = "Neuburg"
    lazy var whole : String = {
        var s = self.first
        s.appendContentsOf(" ")
        s.appendContentsOf(self.last)
        return s
    }
}  

如果屬性是實例屬性(默認情況),那麼只能通過實例來訪問它,並且對於每個實例來說,其值都是獨立的。比如,再來看看這個Dog類:


class Dog {
    let name : String
    let license : Int
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
}  

這個Dog類有一個name實例屬性,接下來可以通過兩個不同的name值創建兩個不同的Dog實例,並通過實例訪問每個Dog的name屬性:


let fido = Dog(name:"Fido", license:1234)
let spot = Dog(name:"Spot", license:1357)
let aName = fido.name // "Fido"
let anotherName = spot.name // "Spot"  

另一方面,靜態/類屬性是通過類型訪問的,其作用域是類型,這意味著它是全局且唯一的,這裡使用一個結構體作為示例:


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
}  

現在,其他地方的代碼可以獲取到Greeting.friendly與Greeting.hostile的值。該示例非常有代表性;不變的靜態/類屬性可以作為一種非常便捷且有效的方式為代碼提供命名空間下的常量。

與實例屬性不同,靜態屬性可以通過對其他靜態屬性的引用進行實例化,這是因為靜態屬性初始化器是延遲的(參見第3章):


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static let ambivalent = friendly + " but " + hostile
}  

注意到上述代碼中沒有使用self。在靜態/類代碼中,self表示類型本身。即便在self會被隱式使用的場景下,我也傾向於顯式使用它,不過這裡卻無法使用self,雖然編譯器不會報錯(我認為這是個Bug)。為了表示friendly與hostile的狀態,我可以使用類型名字,就像其他代碼一樣:


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static let ambivalent = Greeting.friendly + " but " + Greeting.hostile
}  

另外,如果將ambivalent作為計算屬性,那就可以使用self了:


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static var ambivalent : String {
        return self.friendly + " but " + self.hostile
    }
}  

此外,如果初始值是通過定義與調用匿名函數所設置的,那就無法使用self(我認為這也是個Bug):


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static var ambivalent : String = {
        return self.friendly + " but " + self.hostile // compile error
    }
}  

4.1.3 方法

方法就是函數,只是聲明在對像類型聲明頂部的函數,這意味著第2章介紹的關於函數的一切也都適用於方法。

在默認情況下,方法是實例方法,這意味著只能通過實例來進入它。在實例方法體中,self指的就是實例。為了說明這一點,我們繼續在Dog類中添加一些內容:


class Dog {
    let name : String
    let license : Int
    let whatDogsSay = "Woof"
    init(name:String, license:Int) {
        self.name = name
        self.license = license
    }
    func bark {
        print(self.whatDogsSay)
    }
    func speak {
        self.bark
        print("I'm \(self.name)")
    }
}  

現在可以創建Dog實例並調用它的speak方法:


let fido = Dog(name:"Fido", license:1234)
fido.speak // Woof I'm Fido  

在Dog類中,speak方法通過self調用了實例方法bark,然後又通過self獲取到實例屬性name的值;而bark實例方法則通過self獲取到實例屬性whatDogsSay的值。這是因為實例代碼可以通過self引用到該實例;如果引用沒有歧義,那麼代碼就可以省略self;比如,代碼可以寫成這樣:


func speak {
    bark
    print("I'm \(name)")
}  

不過,我從來都不會這麼寫(僅僅是偶爾為之)。我認為,省略self會導致代碼的可讀性與可維護性變差;僅僅使用bark與name看起來會令人費解且困惑。此外,有時self是不可以省略的。比如,在init(name:license:)實現中,我必須得使用self消除參數name與屬性self.name之間的差別。

靜態/類屬性是通過類型訪問的,self表示的是類型。參見如下Greeting結構體示例:


struct Greeting {
    static let friendly = "hello there"
    static let hostile = "go away"
    static var ambivalent : String {
        return self.friendly + " but " + self.hostile
    }
    static func beFriendly {
        print(self.friendly)
    }
}  

下面展示了如何調用靜態方法beFriendly:


Greeting.beFriendly // hello there  

雖然聲明在相同的對象類型中,但靜態/類成員與實例成員之間在概念上還是存在一些差別,它們位於不同的世界中。靜態/類方法不能引用「實例」,因為根本就沒有實例存在;靜態/類方法不能直接引用任何實例屬性,也不能調用任何實例方法。另外,實例方法卻可以通過名字引用類型,也可以訪問靜態/類屬性,調用靜態/類方法(本章後面將會介紹實例方法引用類型的另一種方式)。

比如,回到Dog類上來,解決一下狗會叫的問題。假設所有狗叫的都一樣。因此,我們傾向於在類級別而非實例級別表示whatDogsSay。這正是靜態屬性的用武之地,下面是一個用於說明問題的簡化的Dog類:

實例方法揭秘

有這樣一個秘密:實例方法實際上可以訪問靜態/類方法。比如,如下代碼是合法的(但看起來很奇怪):


class MyClass {
    var s = ""
    func store(s:String) {
        self.s = s
    }
}
let m = MyClass
let f = MyClass.store(m) // what just happened!?  

雖然store是個實例方法,但我們能以類方法的形式調用它,即通過將類實例作為參數!原因在於實例方法實際上是由兩個函數構成的調製靜態/類方法:一個函數接收一個實例,另一個函數接收實例方法的參數。這樣,在上述代碼執行後,f就成為第2個函數,調用它就相當於調用實例m的store方法一樣:


f("howdy")
print(m.s) // howdy
class Dog {
    static var whatDogsSay = "Woof"
    func bark {
        print(Dog.whatDogsSay)
    }
}  

接下來創建一個Dog實例並調用其bark方法:


let fido = Dog
fido.bark // Woof  

4.1.4 下標

下標是一種實例方法,不過調用方式比較特殊:在實例引用後面使用方括號,方括號可以包含傳遞給下標方法的參數。你可以通過該特性做任何想做的事情,不過它特別適合於通過鍵或索引號訪問對像類型中的元素的場景。第3章曾介紹過該語法搭配字符串的使用方式,字典與數組也經常見到這種使用方式;你可以對字符串、字典與數組使用方括號,因為Swift的String、Dictionary與Array類型都聲明了下標方法。

聲明下標方法的語法類似於函數聲明和計算屬性聲明,這並非巧合!下標類似於函數,因為它可以接收參數:當調用下標方法時,實參位於方括號中。下標類似於計算屬性,因為調用就好像是對屬性的引用:你可以獲取其值,也可以對其賦值。

為了說明問題,我聲明一個結構體,它對待整型的方式就像是字符串,通過在方括號中使用索引數的方式返回一個數字;出於簡化的目的,我有意省略了錯誤檢查代碼:


struct Digit {
    var number : Int
    init(_ n:Int) {
        self.number = n
    }
    subscript(ix:Int) -> Int { 12
        get { 3
            let s = String(self.number)
            return Int(String(s[s.startIndex.advancedBy(ix)]))!
        }
    }
}  

1關鍵字subscript後面有一個參數列表,指定什麼參數可以出現在方括號中;在默認情況下,其名字不是外化的。

2接下來,在箭頭運算符後面指定了傳出(調用getter時)或傳入(調用setter時)的值類型;這與計算屬性的類型聲明是類似的,不過箭頭運算符的語法類似於函數聲明中的返回值。

3最後,花括號中的內容就像是計算屬性的內容。你可以為getter提供get與花括號,為setter提供set與花括號。如果只有getter沒有setter,那麼單詞get及後面的花括號就可以省略。setter會將新值作為newValue,不過你可以在圓括號中單詞set後面提供不同的名字來改變它。

下面是調用getter的一個示例;實例名後面跟著方括號,裡面是實參值,調用時相當於獲取一個屬性值一樣:


var d = Digit(1234)
let aDigit = d[1] // 2
  

現在來擴展Digit結構體,使其下標方法包含setter(再次省略錯誤檢查代碼):


struct Digit {
    var number : Int
    init(_ n:Int) {
        self.number = n
    }
    subscript(ix:Int) -> Int {
        get {
            let s = String(self.number)
            return Int(String(s[s.startIndex.advancedBy(ix)]))!
        }
        set {
            var s = String(self.number)
            let i = s.startIndex.advancedBy(ix)
            s.replaceRange(i...i, with: String(newValue))
            self.number = Int(s)!
        }
    }
}
  

下面是調用setter的一個示例;實例名後面跟著方括號,裡面是實參值,調用時相當於設置一個屬性值一樣:


var d = Digit(1234)
d[0] = 2 // now d.number is 2234
  

一個對像類型可以聲明多個下標方法,前提是其簽名不同。

4.1.5 嵌套對像類型

一個對像類型可以聲明在另一個對像類型聲明中,從而形成嵌套類型:


class Dog {
    struct Noise {
        static var noise = "Woof"
    }
    func bark {
        print (Dog.Noise.noise)
    }
}
  

嵌套對像類型與一般的對象類型沒有區別,不過從外部引用它的規則發生了變化;外部對像類型成為一個命名空間,必須要顯式通過它才能訪問到嵌套對像類型:


Dog.Noise.noise = "Arf"
  

Noise結構體位於Dog類命名空間下面,該命名空間增強了清晰性:名字Noise不能隨意使用,必須要顯式關聯到所屬的Dog類。借助命名空間,我們可以創建多個Noise結構體,而不會造成名字衝突。Swift內建對像類型通常都會利用命名空間;比如,有一些結構體包含了Index結構體,而String結構體就是其中之一,它們之間不會造成名字衝突。

(借助於Swift的隱私原則,我們還可以隱藏嵌套對像類型,這樣就無法在外部引用它了。這樣,如果一個對像類型需要另一個對像類型作為輔助,而其他對像類型無須瞭解這個輔助對像類型,那麼通過這種方式就可以很好地起到組織和封裝的目的。第5章將會介紹隱私。)

4.1.6 實例引用

總的來說,對像類型的名字是全局的,只需通過其名字就可以引用它們,不過實例則不同。實例必須要顯式地逐一創建,這正是實例化的目的之所在。創建好實例後,你可以將它存儲到具有足夠長生命週期的變量中以保證實例一直存在;將該變量作為引用,你可以向實例發送實例消息,訪問實例屬性並調用實例方法。

對對像類型的實例化是直接創建該類型全新實例的一種方式,這需要調用初始化器。不過在很多情況下,其他對像會創建對象並將其提供給你。

一個簡單的例子就是像下面這樣操縱一個String時會發生什麼:


let s = "Hello, world"
let s2 = s.uppercaseString
  

上述代碼執行完畢後會生成兩個String實例。第1個s是通過字符串字面值創建的;第2個s2是通過訪問第1個字符串的uppercaseString屬性創建的。因此,我們會得到兩個實例,只要對它們的引用存在,這兩個實例就會存在而且相互獨立;不過,在創建它們時並未調用初始化器。

有時,你所需要的實例已經以某種持久化形式存在了;接下來的問題就在於如何獲得對該實例的引用。

比如,有一個實際的iOS應用。你當然會有一個根視圖控制器,它是某種UIViewController的實例。假設它是ViewController類的實例。當應用啟動並運行後,該實例就已經存在了。接下來,通過實例化ViewController類來與根視圖控制器進行通信顯然與我們的想法是背道而馳的:


let theVC = ViewController
  

上述代碼會創建另一個完全不同的ViewController類實例,向該實例發送的消息都是毫無意義的,因為它並非你想要與之通信的那個特定實例。這是初學者常犯的一個錯誤,請注意。

獲取對已經存在的實例的引用是個很有意思的話題。顯然,實例化並不是解決之道,那該怎麼做呢?要具體問題具體分析。在這個特定的情況下,我們的目標是從代碼中獲取到對應用根視圖控制器實例的引用。下面來介紹一下該怎麼做。

獲取引用總是從你已經具有引用的對象開始,通常這是個類。在iOS編程中,應用本身就是個實例,有一個類會持有一個對該實例的引用,它會在你需要時將其傳遞給你。這個類就是UIApplication,我們可以通過調用其sharedApplication類方法來獲得對應用實例的引用:


let app = UIApplication.sharedApplication
  

現在,我們擁有了對應用實例的引用,該應用實例有一個keyWindow屬性:


let window = app.keyWindow
  

現在,我們有了對應用主窗口的引用。該窗口擁有根視圖控制器,並且會將對其的引用給我們,即其rootViewController屬性;應用的keyWindow是個Optional,因此需要將其展開才能得到rootViewController:


let vc = window?.rootViewController
  

現在,我們有了對應用根視圖控制器的引用。為了獲得對該持久化實例的引用,我們實際上創建了一個方法調用與屬性鏈,從已知到未知,從全局類到特定實例:


let app = UIApplication.sharedApplication
let window = app.keyWindow
let vc = window?.rootViewController
  

顯然,可以通過一個鏈來表示上述代碼,使用重複的點符號即可:


let vc = UIApplication.sharedApplication.keyWindow?.rootViewController
  

無需將實例消息鏈接為單獨一行:使用多個let賦值會更具效率、更加清晰、也更易於調試。不過這麼做會更加便捷,也是Swift這種使用點符號的面向對像語言的一個特性。

獲取對已經存在的實例的引用是個很有趣的話題,應用也非常廣泛,第13章將會對其進行深入介紹。